diff options
Diffstat (limited to 'testing/web-platform/tests/webxr')
156 files changed, 10801 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webxr/META.yml b/testing/web-platform/tests/webxr/META.yml new file mode 100644 index 0000000000..13c5d7b595 --- /dev/null +++ b/testing/web-platform/tests/webxr/META.yml @@ -0,0 +1,3 @@ +spec: https://immersive-web.github.io/webxr/ +suggested_reviewers: + - klausw diff --git a/testing/web-platform/tests/webxr/anchors/META.yml b/testing/web-platform/tests/webxr/anchors/META.yml new file mode 100644 index 0000000000..0b7f740ac5 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/anchors/ diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_create_move.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_create_move.https.html new file mode 100644 index 0000000000..87f7556007 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_create_move.https.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// All test cases require anchors. +const sessionInit = { 'requiredFeatures': ['anchors'] }; + +// Create an anchor, move it and verify that new pose gets propagated to the caller. +const anchorCreateAndMove = function(session, fakeDeviceController, t) { + const debug = xr_debug.bind(this, 'anchorCreateAndMove'); + + let anchorController = null; + fakeDeviceController.setAnchorCreationCallback((parameters, controller) => { + anchorController = controller; + return Promise.resolve(true); + }); + + const watcherDone = new Event("watcherdone"); + const eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + const eventPromise = eventWatcher.wait_for(["watcherdone"]); + + session.requestReferenceSpace('local').then((localRefSpace) => { + debug("requesting animation frame"); + + session.requestAnimationFrame((time, frame) => { + debug("rAF 1"); + + let createdAnchor = null; + frame.createAnchor(new XRRigidTransform(), localRefSpace) + .then((anchor) => { + createdAnchor = anchor; + }); + + session.requestAnimationFrame((time_2, frame_2) => { + debug("rAF 2"); + + const pre_move_pose = frame_2.getPose(createdAnchor.anchorSpace, localRefSpace); + + t.step(() => { + assert_true(frame_2.trackedAnchors.has(createdAnchor), + "Newly created anchor must be in tracked anchors set on subsequent RAF (2)!"); + // We have created an anchor with an identity pose relative to local space and have not moved it yet: + assert_matrix_approx_equals(pre_move_pose.transform.matrix, + IDENTITY_MATRIX, FLOAT_EPSILON); + }); + + anchorController.setAnchorOrigin(VALID_POSE_TRANSFORM); + + session.requestAnimationFrame((time_3, frame_3) => { + debug("rAF 3"); + + const post_move_pose = frame_3.getPose(createdAnchor.anchorSpace, localRefSpace); + + t.step(() => { + assert_true(frame_3.trackedAnchors.has(createdAnchor), + "Newly created anchor must be in tracked anchors set on subsequent RAF (3)!"); + // The anchor was moved by VALID_POSE_TRANSFORM, validate that its pose got adjusted: + assert_matrix_approx_equals(post_move_pose.transform.matrix, + VALID_POSE_MATRIX, FLOAT_EPSILON); + }); + + session.dispatchEvent(watcherDone); + }); + }); + }); + }); // session.requestReferenceSpace(...).then({...}); + + return eventPromise; +}; + +xr_session_promise_test( + "Ensures free-floating anchor move gets propagated to anchor poses", + anchorCreateAndMove, fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_delay_creation.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_delay_creation.https.html new file mode 100644 index 0000000000..686eacf848 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_delay_creation.https.html @@ -0,0 +1,122 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// All test cases require anchors. +const sessionInit = { 'requiredFeatures': ['anchors'] }; + +// Creates a test function that will attempt to create an anchor with a delay. +// In case the anchor creation is expected to succeed, the test will then +// validate whether the anchor belongs to frame.trackedAnchors, has a valid pose, +// and that after deleting it, it no longer allows access to its anchorSpace. +// |shouldSucceed| - true if the anchor creation is expected to succeed. +const anchorCreationDelayedCreator = function(shouldSucceed) { + return function(session, fakeDeviceController, t) { + const debug = xr_debug.bind(this, 'anchorCreationDelayed' + (shouldSucceed ? 'Success' : 'Failure')); + + let anchorCreationResolve = null; + fakeDeviceController.setAnchorCreationCallback((parameters, controller) => { + return new Promise((resolve) => { + anchorCreationResolve = resolve; + }); + }); + + const watcherDone = new Event("watcherdone"); + const creationDelayedEvent = new Event("creationdelayed"); + const eventWatcher = new EventWatcher(t, session, ["creationdelayed", "watcherdone"]); + const eventPromise = eventWatcher.wait_for(["creationdelayed", "watcherdone"]); + + session.requestReferenceSpace('local').then((localRefSpace) => { + debug("requesting animation frame"); + + session.requestAnimationFrame((time, frame) => { + debug("rAF 1"); + + let createdAnchor = null; + frame.createAnchor(new XRRigidTransform(), localRefSpace) + .then((anchor) => { + createdAnchor = anchor; + + t.step(() => { + assert_true(anchor != null, "Returned anchor should not be null!"); + assert_true(shouldSucceed, + "Anchor creation succeeded when it was expected to fail!"); + }); + }) + .catch((error) => { + t.step(() => { + assert_false(shouldSucceed, + "Anchor creation failed when it was expected to succeed!"); + }); + + session.dispatchEvent(watcherDone); + }); + + session.requestAnimationFrame(() => { + debug("rAF 2"); + + session.dispatchEvent(creationDelayedEvent); + + anchorCreationResolve(shouldSucceed); + + session.requestAnimationFrame((time_2, frame_2) => { + debug("rAF 3"); + + if(shouldSucceed) { + t.step(() => { + assert_true(createdAnchor != null); + assert_true(frame_2.trackedAnchors.has(createdAnchor), + "Newly created anchor must be in tracked anchors set!"); + assert_true(createdAnchor.anchorSpace != null, + "Newly created anchor must have a non-null anchor space!"); + assert_true(frame_2.getPose(createdAnchor.anchorSpace, localRefSpace) != null, + "Newly created anchor should have a pose!"); + }); + + createdAnchor.delete(); + + t.step(() => { + assert_throws_dom('InvalidStateError', () => { + createdAnchor.anchorSpace; + }); + }); + + session.dispatchEvent(watcherDone); + } + }); + }); + }); + }); // session.requestReferenceSpace(...).then({...}); + + return eventPromise; + }; +}; + +xr_session_promise_test( + "Ensures free-floating anchor creation with delayed success is handled correctly", + anchorCreationDelayedCreator(true), fakeDeviceInitParams, 'immersive-ar', sessionInit); + +xr_session_promise_test( + "Ensures free-floating anchor creation with delayed failure is handled correctly", + anchorCreationDelayedCreator(false), fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_failure.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_failure.https.html new file mode 100644 index 0000000000..dd03be0ad5 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_failure.https.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// All test cases require anchors. +const sessionInit = { 'requiredFeatures': ['anchors'] }; + +// Fail the anchor creation & see if it gets communicated to the caller. +// The concrete error is not specified by the WebXR Test API / WebXR Anchors. +const anchorCreationFail = function(session, fakeDeviceController, t) { + const debug = xr_debug.bind(this, 'anchorCreationFail'); + + fakeDeviceController.setAnchorCreationCallback((parameters, controller) => { + // Immediately fail anchor creation. + return Promise.resolve(false); + }); + + const watcherDone = new Event("watcherdone"); + const eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + const eventPromise = eventWatcher.wait_for(["watcherdone"]); + + session.requestReferenceSpace('local').then((localRefSpace) => { + debug("requesting animation frame"); + + session.requestAnimationFrame((time, frame) => { + debug("rAF 1"); + + frame.createAnchor(new XRRigidTransform(), localRefSpace) + .then((anchor) => { + t.step(() => { + assert_false(true, "Anchor creation should fail!"); + }); + }) + .catch((error) => { + session.dispatchEvent(watcherDone); + }); + + // Anchor result will only take effect with frame data - schedule + // a frame after we requested anchor creation, otherwise the test will time out. + session.requestAnimationFrame(() => { + debug("rAF 2"); + }); + }); + }); // session.requestReferenceSpace(...).then({...}); + + return eventPromise; +} + +xr_session_promise_test( + "Ensures free-floating anchor creation failure is handled correctly", + anchorCreationFail, fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_pause_resume_stop.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_pause_resume_stop.https.html new file mode 100644 index 0000000000..6eb6bc76e6 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_pause_resume_stop.https.html @@ -0,0 +1,115 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// All test cases require anchors. +const sessionInit = { 'requiredFeatures': ['anchors'] }; + +// Create an anchor, pause tracking, resume tracking, & stop tracking and validate +// that the state changes are propagated to the caller. +const anchorCreatePauseTrackingResumeAndDelete = function(session, fakeDeviceController, t) { + const debug = xr_debug.bind(this, 'anchorCreatePauseTrackingResumeAndDelete'); + + let anchorController = null; + fakeDeviceController.setAnchorCreationCallback((parameters, controller) => { + anchorController = controller; + return Promise.resolve(true); + }); + + const watcherDone = new Event("watcherdone"); + const eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + const eventPromise = eventWatcher.wait_for(["watcherdone"]); + + session.requestReferenceSpace('local').then((localRefSpace) => { + debug("requesting animation frame"); + + session.requestAnimationFrame((time, frame) => { + debug("rAF 1"); + + let createdAnchor = null; + frame.createAnchor(new XRRigidTransform(), localRefSpace) + .then((anchor) => { + createdAnchor = anchor; + }); + + session.requestAnimationFrame((time_2, frame_2) => { + debug("rAF 2"); + + t.step(() => { + assert_true(frame_2.getPose(createdAnchor.anchorSpace, localRefSpace) != null, + "Newly created anchor should have a pose!"); + assert_true(frame_2.trackedAnchors.has(createdAnchor), + "Newly created anchor must be in tracked anchors set on subsequent RAF (2)!"); + }); + + anchorController.pauseTracking(); + + session.requestAnimationFrame((time_3, frame_3) => { + debug("rAF 3"); + + t.step(() => { + assert_true(frame_3.getPose(createdAnchor.anchorSpace, localRefSpace) == null, + "Newly created anchor with paused tracking should not have a pose!"); + assert_true(frame_3.trackedAnchors.has(createdAnchor), + "Newly created anchor with paused tracking must be in tracked anchors set on subsequent RAF (3)!"); + }); + + anchorController.resumeTracking(); + + session.requestAnimationFrame((time_4, frame_4) => { + debug("rAF 4"); + + t.step(() => { + assert_true(frame_4.trackedAnchors.has(createdAnchor), + "Newly created anchor with resumed tracking must be in tracked anchors set on subsequent RAF (4)!"); + assert_true(frame_4.getPose(createdAnchor.anchorSpace, localRefSpace) != null, + "Newly created anchor with resumed tracking should have a pose!"); + }); + + anchorController.stopTracking(); + + session.requestAnimationFrame((time_5, frame_5) => { + debug("rAF 5"); + + t.step(() => { + assert_false(frame_5.trackedAnchors.has(createdAnchor), + "Newly created anchor with stopped tracking must not be in tracked anchors set on subsequent RAF (5)!"); + }); + + session.dispatchEvent(watcherDone); + }); + }); + }); + }); + }); + }); // session.requestReferenceSpace(...).then({...}); + + return eventPromise; +}; + + +xr_session_promise_test( + "Ensures free-floating anchor state changes get propagated", + anchorCreatePauseTrackingResumeAndDelete, fakeDeviceInitParams, + 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_getAnchors.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_getAnchors.https.html new file mode 100644 index 0000000000..50664b0a4b --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_getAnchors.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// Attempts to access XRFrame.trackedAnchors and expects to get empty set +// since no anchors are being created. +const testFunction = function(session, fakeDeviceController, t) { + const debug = xr_debug.bind(this, 'testGetAnchors'); + + let done = false; + + session.requestReferenceSpace('local').then((localRefSpace) => { + const onFrame = function(time, frame) { + const trackedAnchors = frame.trackedAnchors; + t.step(() => { + assert_equals(trackedAnchors.size, 0); + }); + + done = true; + }; + + session.requestAnimationFrame(onFrame); + }); // session.requestReferenceSpace(...) + + return t.step_wait(() => done); +}; // testFunction + + +xr_session_promise_test("XRFrame's trackedAnchors is empty when the feature was not requested", + testFunction, + fakeDeviceInitParams, + 'immersive-ar', { }); + +xr_session_promise_test("XRFrame's trackedAnchors is empty when the feature was requested", + testFunction, + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['anchors'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/ar_anchor_states.https.html b/testing/web-platform/tests/webxr/anchors/ar_anchor_states.https.html new file mode 100644 index 0000000000..8369549852 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/ar_anchor_states.https.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, +}; + +// Creates a test method that leverages anchors API. +// |expectSucceeded| - true if the anchors creation request is expected to succeed, false otherwise +// |endSession| - true if the test case should call session.end() prior to creating an anchor +// |expectedError| - expected error name that should be returned in case expectSucceeded is false +const testFunctionGenerator = function(expectSucceeded, endSession, expectedError) { + + const testFunction = function(session, fakeDeviceController, t) { + + const debug = xr_debug.bind(this, 'testAnchorStates'); + + fakeDeviceController.setAnchorCreationCallback((parameters, controller) => { + // All anchor creation requests that reach this stage should be marked as successful. + // If this test is expected to fail, the failure will happen earlier in the anchor + // creation process. + return Promise.resolve(true); + }); + + const watcherDone = new Event("watcherdone"); + const eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + const eventPromise = eventWatcher.wait_for(["watcherdone"]); + + session.requestReferenceSpace('local').then((localRefSpace) => { + + const onFrame = function(time, frame) { + debug("rAF 1"); + + let setUpPromise = Promise.resolve(); + if(endSession) { + debug("ending session"); + setUpPromise = session.end(); + } + + setUpPromise.then(() => { + debug("creating anchor"); + frame.createAnchor(new XRRigidTransform(), localRefSpace) + .then((anchor) => { + debug("anchor created"); + + t.step(() => { + assert_true(expectSucceeded, + "`createAnchor` succeeded when it was expected to fail"); + }); + + session.dispatchEvent(watcherDone); + }).catch((error) => { + debug("anchor creation failed"); + + t.step(() => { + assert_false(expectSucceeded, + "`createAnchor` failed when it was expected to succeed, error: " + error); + assert_equals(error.name, expectedError, + "`createAnchor` failed with unexpected error name"); + }); + + session.dispatchEvent(watcherDone); + }); + + // Anchor result will only take effect with frame data - schedule + // a frame after we requested anchor creation, otherwise the test will time out. + session.requestAnimationFrame(() => { + debug("rAF 2"); + }); + }); // setUpPromise.then(() => { ... }) + }; // onFrame() { ... } + + debug("requesting animation frame"); + session.requestAnimationFrame(onFrame); + }); // session.requestReferenceSpace(...) + + return eventPromise; + }; // testFunction + + return testFunction; +}; + +xr_session_promise_test("Anchor creation succeeds if the feature was requested", + testFunctionGenerator(/*expectSucceeded=*/true, /*endSession=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['anchors'] }); + +xr_session_promise_test("Anchor creation fails if the feature was not requested", + testFunctionGenerator(/*expectSucceeded=*/false, /*endSession=*/false, "NotSupportedError"), + fakeDeviceInitParams, + 'immersive-ar', {}); + +xr_session_promise_test("Anchor creation fails if the feature was requested but the session already ended", + testFunctionGenerator(/*expectSucceeded=*/false, /*endSession=*/true, "InvalidStateError"), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['anchors'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/anchors/idlharness.https.window.js b/testing/web-platform/tests/webxr/anchors/idlharness.https.window.js new file mode 100644 index 0000000000..b69a327229 --- /dev/null +++ b/testing/web-platform/tests/webxr/anchors/idlharness.https.window.js @@ -0,0 +1,16 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://immersive-web.github.io/anchors/ + +idl_test( + ['anchors'], + ['webxr-hit-test', 'webxr', 'dom'], + async idl_array => { + idl_array.add_objects({ + // TODO: Add object instances + }); + } +); diff --git a/testing/web-platform/tests/webxr/ar-module/META.yml b/testing/web-platform/tests/webxr/ar-module/META.yml new file mode 100644 index 0000000000..47e5ea9cd4 --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/webxr-ar-module/ diff --git a/testing/web-platform/tests/webxr/ar-module/idlharness.https.window.js b/testing/web-platform/tests/webxr/ar-module/idlharness.https.window.js new file mode 100644 index 0000000000..51cc3c4280 --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/idlharness.https.window.js @@ -0,0 +1,17 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://immersive-web.github.io/webxr-ar-module/ + +idl_test( + ['webxr-ar-module'], + ['webxr', 'dom'], + async idl_array => { + idl_array.add_objects({ + XRSession: ['xrSession'], + }); + self.xrSession = await navigator.xr.requestSession('inline'); + } +); diff --git a/testing/web-platform/tests/webxr/ar-module/xrDevice_isSessionSupported_immersive-ar.https.html b/testing/web-platform/tests/webxr/ar-module/xrDevice_isSessionSupported_immersive-ar.https.html new file mode 100644 index 0000000000..2cd36a17f6 --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/xrDevice_isSessionSupported_immersive-ar.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "isSessionSupported resolves to true for immersive-ar on a supported device", + (t) => { + return navigator.xr.test.simulateDeviceConnection(IMMERSIVE_AR_DEVICE) + .then( (controller) => { + return navigator.xr.isSessionSupported('immersive-ar').then((supported) => { + t.step(() => { + assert_true(supported); + }); + }); + }); + }); + + xr_promise_test( + "isSessionSupported resolves to false for immersive-ar on an unsupported device", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => { + return navigator.xr.isSessionSupported('immersive-ar').then((supported) => { + t.step(() => { + assert_false(supported); + }); + }); + }); + }); +</script> +</body> diff --git a/testing/web-platform/tests/webxr/ar-module/xrDevice_requestSession_immersive-ar.https.html b/testing/web-platform/tests/webxr/ar-module/xrDevice_requestSession_immersive-ar.https.html new file mode 100644 index 0000000000..9984ee589d --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/xrDevice_requestSession_immersive-ar.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + <script> + xr_session_promise_test( + "Tests requestSession accepts immersive-ar mode", + (session) => { + assert_not_equals(session, null); + }, IMMERSIVE_AR_DEVICE, 'immersive-ar', {}); + + xr_promise_test( + "Tests requestSession rejects immersive-ar mode when unsupported", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then((controller) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation(() => { + resolve(promise_rejects_dom( + t, "NotSupportedError", + navigator.xr.requestSession('immersive-ar', {}))); + }); + })); + }); +</script> +</body> diff --git a/testing/web-platform/tests/webxr/ar-module/xrSession_environmentBlendMode.https.html b/testing/web-platform/tests/webxr/ar-module/xrSession_environmentBlendMode.https.html new file mode 100644 index 0000000000..beff66586c --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/xrSession_environmentBlendMode.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + <script> + xr_session_promise_test( + "Tests environmentBlendMode for an AR device", + (session) => { + assert_not_equals(session.environmentBlendMode, "opaque"); + assert_in_array(session.environmentBlendMode, ["alpha-blend", "additive"]); + }, IMMERSIVE_AR_DEVICE, 'immersive-ar', {}); + + + xr_session_promise_test( + "Tests environmentBlendMode for a VR device", + (session) => { + assert_not_equals(session.environmentBlendMode, "alpha-blend"); + assert_in_array(session.environmentBlendMode, ["opaque", "additive"]); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {}); +</script> +</body>
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/ar-module/xrSession_interactionMode.https.html b/testing/web-platform/tests/webxr/ar-module/xrSession_interactionMode.https.html new file mode 100644 index 0000000000..89cdc80132 --- /dev/null +++ b/testing/web-platform/tests/webxr/ar-module/xrSession_interactionMode.https.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + <canvas></canvas> + <script> + const VR_HMD_DEVICE = { + supportsImmersive: true, + supportedModes: [ "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "world-space" + }; + xr_session_promise_test( + "Tests interactionMode for an VR_HMD_DEVICE", + (session) => { + assert_equals(session.interactionMode, "world-space"); + }, VR_HMD_DEVICE, 'immersive-vr', {}); + + const VR_SCREEN_DEVICE = { + supportsImmersive: true, + supportedModes: [ "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "screen-space" + }; + xr_session_promise_test( + "Tests interactionMode for an VR_SCREEN_DEVICE", + (session) => { + assert_equals(session.interactionMode, "screen-space"); + }, VR_SCREEN_DEVICE, 'immersive-vr', {}); + + const AR_HMD_DEVICE = { + supportsImmersive: true, + supportedModes: [ "immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "additive", + interactionMode: "world-space" + }; + xr_session_promise_test( + "Tests interactionMode for an AR_HMD_DEVICE", + (session) => { + assert_equals(session.interactionMode, "world-space"); + }, AR_HMD_DEVICE, 'immersive-ar', {}); + + const AR_SCREEN_DEVICE = { + supportsImmersive: true, + supportedModes: [ "immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "screen-space" + }; + xr_session_promise_test( + "Tests interactionMode for an AR_SCREEN_DEVICE", + (session) => { + assert_equals(session.interactionMode, "screen-space"); + }, AR_SCREEN_DEVICE, 'immersive-ar', {}); + + const INLINE_SCREEN_DEVICE = { + supportsImmersive: true, + supportedModes: [ "inline"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "screen-space" + }; + xr_session_promise_test( + "Tests interactionMode for a INLINE_SCREEN_DEVICE", + (session) => { + assert_equals(session.interactionMode, "screen-space"); + }, INLINE_SCREEN_DEVICE, 'inline', {}); +</script> +</body> diff --git a/testing/web-platform/tests/webxr/camera-access/xrCamera_resolution.https.html b/testing/web-platform/tests/webxr/camera-access/xrCamera_resolution.https.html new file mode 100644 index 0000000000..d6d8690807 --- /dev/null +++ b/testing/web-platform/tests/webxr/camera-access/xrCamera_resolution.https.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +const test_camera_present = (session, fakeDeviceController, t) => { + return session.requestReferenceSpace('viewer').then(viewerRefSpace => { + return new Promise((resolve, reject) => { + const requestAnimationFrameCallbackWithCamera = (time, frame) => { + const viewerPose = frame.getViewerPose(viewerRefSpace); + + t.step(() => { + let foundCamera = false; + + assert_not_equals(viewerPose, null, "Viewer pose should not be null!"); + assert_equals(viewerPose.views.length, VALID_VIEWS.length, "View lengths should match!"); + + for (const view of viewerPose.views) { + if (view.camera) { + assert_equals(view.camera.width, 333, "Width doesn't match expectations!"); + assert_equals(view.camera.height, 444, "Height doesn't match expectations!"); + + foundCamera = true; + } + } + + assert_true(foundCamera, "There should be at least one camera! Didn't find any.") + }); + + resolve(); + }; + + const requestAnimationFrameCallbackNoCamera = (time, frame) => { + + const viewerPose = frame.getViewerPose(viewerRefSpace); + t.step(() => { + assert_not_equals(viewerPose, null, "Viewer pose should not be null!"); + assert_equals(viewerPose.views.length, VALID_VIEWS.length, "View lengths should match!"); + + for (const view of viewerPose.views) { + assert_equals(view.camera, null, "Camera should be null!"); + } + }); + + const views_with_camera = VALID_VIEWS.map((element, index) => { + return { + ...element, + resolution: { width: 111 * (index+1), height: 222 * (index+1)}, + cameraImageInit: { width: 333, height: 444 }, + }; + }); + fakeDeviceController.setViews(views_with_camera); + + // After this rAFcb, the test is supposed to continue w/ a callback that + // expects XRCamera to be present: + session.requestAnimationFrame(requestAnimationFrameCallbackWithCamera); + }; + + // Kick off the test - start with rAFcb w/o an XRCamera: + session.requestAnimationFrame(requestAnimationFrameCallbackNoCamera); + }); + }); +}; + +xr_session_promise_test("XRCamera object is present and carries expected dimensions", + test_camera_present, + fakeDeviceInitParams, 'immersive-ar', {'requiredFeatures': ['camera-access']}); + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_dataUnavailable.https.html b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_dataUnavailable.https.html new file mode 100644 index 0000000000..e120f0b7dd --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_dataUnavailable.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../dataUnavailableTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + depthSensingData: DEPTH_SENSING_DATA, +}; + +xr_session_promise_test("Ensures depth data is not available when cleared in the controller, `cpu-optimized`", + dataUnavailableTestFunctionGenerator(/*isCpuOptimized=*/true), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_CPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_inactiveFrame.https.html b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_inactiveFrame.https.html new file mode 100644 index 0000000000..92c20cecbf --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_inactiveFrame.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../inactiveFrameTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +xr_session_promise_test("Ensures getDepthInformation() throws when not run in an active frame, `cpu-optimized`", + inactiveFrameTestFunctionGenerator(/*isCpuOptimized=*/true), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_CPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_incorrectUsage.https.html b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_incorrectUsage.https.html new file mode 100644 index 0000000000..44868d8cb0 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_incorrectUsage.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../incorrectUsageTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +const incorrectUsagetestFunctionTryGetWebGLOnCpu = function (session, controller, t, sessionObjects) { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + const glBinding = new XRWebGLBinding(session, sessionObjects.gl); + + session.requestAnimationFrame((time, frame) => { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + t.step(() => { + assert_throws_dom("InvalidStateError", () => glBinding.getDepthInformation(view), + "XRWebGLBinding.getDepthInformation() should throw when depth sensing is in `cpu-optimized` usage mode"); + }); + } + + done = true; + }); + + return t.step_wait(() => done); + }); +}; + +xr_session_promise_test("Ensures XRWebGLDepthInformation is not obtainable in `cpu-optimized` usage mode", + incorrectUsagetestFunctionTryGetWebGLOnCpu, + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_CPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_luminance_alpha_dataValid.https.html b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_luminance_alpha_dataValid.https.html new file mode 100644 index 0000000000..db88c42558 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_luminance_alpha_dataValid.https.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_math_utils.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + depthSensingData: DEPTH_SENSING_DATA, +}; + +const assert_depth_valid_at = function(depthInformation, row, column, deltaRow, deltaColumn) { + // column and row correspond to the depth buffer coordinates, + // *not* to normalized view coordinates the getDepthInMeters() expects. + + const expectedValue = getExpectedValueAt(column, row); + + // 1. Normalize: + let x = (column + deltaColumn) / depthInformation.width; + let y = (row + deltaRow) / depthInformation.height; + + // 2. Apply the transform that changes the origin and axes: + x = 1.0 - x; + y = 1.0 - y; + + const depthValue = depthInformation.getDepthInMeters(x, y); + assert_approx_equals(depthValue, expectedValue, FLOAT_EPSILON, + "Depth value at (" + column + "," + row + "), deltas=(" + deltaColumn + ", " + deltaRow + "), " + + "coordinates (" + x + "," + y + ") must match!"); +} + +const assert_depth_valid = function(depthInformation) { + + assert_true(depthInformation.data instanceof ArrayBuffer, + "XRCpuDepthInformation.data must be of type `ArrayBuffer`!"); + + for(let row = 0; row < depthInformation.height; row++) { + for(let column = 0; column < depthInformation.width; column++) { + // middle of the pixel: + assert_depth_valid_at(depthInformation, row, column, 0.5, 0.5); + + // corners of the pixel: + assert_depth_valid_at(depthInformation, row, column, FLOAT_EPSILON, FLOAT_EPSILON); + assert_depth_valid_at(depthInformation, row, column, FLOAT_EPSILON, 1 - FLOAT_EPSILON); + assert_depth_valid_at(depthInformation, row, column, 1 - FLOAT_EPSILON, FLOAT_EPSILON); + assert_depth_valid_at(depthInformation, row, column, 1 - FLOAT_EPSILON, 1 - FLOAT_EPSILON); + } + } + + // Verify out-of-bounds accesses throw: + assert_throws_js(RangeError, + () => depthInformation.getDepthInMeters(-FLOAT_EPSILON, 0.0), + "getDepthInMeters() should throw when run with invalid indices - negative x"); + assert_throws_js(RangeError, + () => depthInformation.getDepthInMeters(0.0, -FLOAT_EPSILON), + "getDepthInMeters() should throw when run with invalid indices - negative y"); + assert_throws_js(RangeError, + () => depthInformation.getDepthInMeters(1+FLOAT_EPSILON, 0.0), + "getDepthInMeters() should throw when run with invalid indices - too big x"); + assert_throws_js(RangeError, + () => depthInformation.getDepthInMeters(0.0, 1+FLOAT_EPSILON), + "getDepthInMeters() should throw when run with invalid indices - too big y"); +}; + +const testCpuOptimizedLuminanceAlpha = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + const rafCallback = function(time, frame) { + const pose = frame.getViewerPose(viewerSpace); + if(pose) { + for(const view of pose.views) { + const depthInformation = frame.getDepthInformation(view); + + t.step(() => { + assert_not_equals(depthInformation, null, "XRCPUDepthInformation must not be null!"); + assert_approx_equals(depthInformation.width, DEPTH_SENSING_DATA.width, FLOAT_EPSILON); + assert_approx_equals(depthInformation.height, DEPTH_SENSING_DATA.height, FLOAT_EPSILON); + assert_approx_equals(depthInformation.rawValueToMeters, DEPTH_SENSING_DATA.rawValueToMeters, FLOAT_EPSILON); + assert_transform_approx_equals(depthInformation.normDepthBufferFromNormView, DEPTH_SENSING_DATA.normDepthBufferFromNormView); + assert_depth_valid(depthInformation); + }); + } + } + + done = true; + }; + + session.requestAnimationFrame(rafCallback); + + return t.step_wait(() => done); + }); +}; + +xr_session_promise_test("Ensures depth data is returned and values match expectation, cpu-optimized, luminance-alpha.", + testCpuOptimizedLuminanceAlpha, + fakeDeviceInitParams, + 'immersive-ar', { + 'requiredFeatures': ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_CPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_staleView.https.html b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_staleView.https.html new file mode 100644 index 0000000000..6a411ace45 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_staleView.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../staleViewsTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +xr_session_promise_test("Ensures getDepthInformation() throws when run with stale XRView, `cpu-optimized`", + staleViewsTestFunctionGenerator(/*isCpuOptimized=*/true), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_CPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/dataUnavailableTests.js b/testing/web-platform/tests/webxr/depth-sensing/dataUnavailableTests.js new file mode 100644 index 0000000000..7460af7132 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/dataUnavailableTests.js @@ -0,0 +1,58 @@ +'use strict'; + +const TestStates = Object.freeze({ + "ShouldSucceedScheduleRAF": 1, + "ShouldFailScheduleRAF": 2, + "ShouldSucceedTestDone": 3, +}); + +const dataUnavailableTestFunctionGenerator = function(isCpuOptimized) { + return (session, controller, t, sessionObjects) => { + let state = TestStates.ShouldSucceedScheduleRAF; + + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + const glBinding = new XRWebGLBinding(session, sessionObjects.gl); + + const rafCb = function(time, frame) { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + const depthInformation = isCpuOptimized ? frame.getDepthInformation(view) + : glBinding.getDepthInformation(view); + + if (state == TestStates.ShouldSucceedScheduleRAF + || state == TestStates.ShouldSucceedTestDone) { + t.step(() => { + assert_not_equals(depthInformation, null); + }); + } else { + t.step(() => { + assert_equals(depthInformation, null); + }); + } + } + + switch(state) { + case TestStates.ShouldSucceedScheduleRAF: + controller.clearDepthSensingData(); + state = TestStates.ShouldFailScheduleRAF; + session.requestAnimationFrame(rafCb); + break; + case TestStates.ShouldFailScheduleRAF: + controller.setDepthSensingData(DEPTH_SENSING_DATA); + state = TestStates.ShouldSucceedTestDone; + session.requestAnimationFrame(rafCb); + break; + case TestStates.ShouldSucceedTestDone: + done = true; + break; + } + }; + + session.requestAnimationFrame(rafCb); + + return t.step_wait(() => done); + }); + }; +};
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/depth-sensing/depth_sensing_notEnabled.https.html b/testing/web-platform/tests/webxr/depth-sensing/depth_sensing_notEnabled.https.html new file mode 100644 index 0000000000..23bae35493 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/depth_sensing_notEnabled.https.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> + +<script> + +const testFunctionCpu = function (session, controller, t) { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + session.requestAnimationFrame((time, frame) => { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + assert_throws_dom("NotSupportedError", () => frame.getDepthInformation(view), + "getDepthInformation() should throw when depth sensing is disabled"); + } + + done = true; + }); + + return t.step_wait(() => done); + }); +}; + +const testFunctionGpu = function (session, controller, t, sessionObjects) { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + const glBinding = new XRWebGLBinding(session, sessionObjects.gl); + + session.requestAnimationFrame((time, frame) => { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + t.step(() => { + assert_throws_dom("NotSupportedError", () => glBinding.getDepthInformation(view), + "getDepthInformation() should throw when depth sensing is disabled"); + }); + } + + done = true; + }); + + return t.step_wait(() => done); + }); +}; + +xr_session_promise_test( + "XRFrame.getDepthInformation() rejects if depth sensing is not enabled on a session", + testFunctionCpu, + IMMERSIVE_AR_DEVICE, + 'immersive-ar'); + +xr_session_promise_test( + "XRWebGLBinding.getDepthInformation() rejects if depth sensing is not enabled on a session", + testFunctionGpu, + IMMERSIVE_AR_DEVICE, + 'immersive-ar'); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_dataUnavailable.https.html b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_dataUnavailable.https.html new file mode 100644 index 0000000000..018edf7693 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_dataUnavailable.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../dataUnavailableTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, + depthSensingData: DEPTH_SENSING_DATA, +}; + +xr_session_promise_test("Ensures depth data is not available when cleared in the controller, `gpu-optimized`", + dataUnavailableTestFunctionGenerator(/*isCpuOptimized=*/false), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_GPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_inactiveFrame.https.html b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_inactiveFrame.https.html new file mode 100644 index 0000000000..6116f7a041 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_inactiveFrame.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../inactiveFrameTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +xr_session_promise_test("Ensures getDepthInformation() throws when not run in an active frame, `gpu-optimized`", + testFunctionGenerator(/*isCpuOptimized=*/false), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_GPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_incorrectUsage.https.html b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_incorrectUsage.https.html new file mode 100644 index 0000000000..9fc2e6a5f8 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_incorrectUsage.https.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../incorrectUsageTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +const incorrectUsageTestFunctionTryGetCpuOnGpu = function (session, controller, t, sessionObjects) { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let done = false; + + session.requestAnimationFrame((time, frame) => { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + t.step(() => { + assert_throws_dom("InvalidStateError", () => frame.getDepthInformation(view), + "XRFrame.getDepthInformation() should throw when depth sensing is in `gpu-optimized` usage mode"); + }); + } + + done = true; + }); + + return t.step_wait(() => done); + }); +}; + +xr_session_promise_test("Ensures XRCPUDepthInformation is not obtainable in `gpu-optimized` usage mode", + incorrectUsageTestFunctionTryGetCpuOnGpu, + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_GPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_staleView.https.html b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_staleView.https.html new file mode 100644 index 0000000000..ecd0d479f7 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_staleView.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../../resources/webxr_util.js"></script> +<script src="../../resources/webxr_test_asserts.js"></script> +<script src="../../resources/webxr_test_constants.js"></script> +<script src="../../resources/webxr_test_constants_fake_depth.js"></script> +<script src="../staleViewsTests.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +xr_session_promise_test("Ensures getDepthInformation() throws when not run with stale XRView, `gpu-optimized`", + staleViewsTestFunctionGenerator(/*isCpuOptimized=*/false), + fakeDeviceInitParams, + 'immersive-ar', { + requiredFeatures: ['depth-sensing'], + depthSensing: VALID_DEPTH_CONFIG_GPU_USAGE, + }); + +</script> diff --git a/testing/web-platform/tests/webxr/depth-sensing/inactiveFrameTests.js b/testing/web-platform/tests/webxr/depth-sensing/inactiveFrameTests.js new file mode 100644 index 0000000000..b310f01ef8 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/inactiveFrameTests.js @@ -0,0 +1,36 @@ +'use strict'; + +const inactiveFrameTestFunctionGenerator = function(isCpuOptimized) { + return (session, controller, t, sessionObjects) => { + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + let callbacksKickedOff = false; + let callbackCounter = 0; + + const glBinding = new XRWebGLBinding(session, sessionObjects.gl); + + const rafCb = function(time, frame) { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + const callback = () => { + t.step(() => { + assert_throws_dom("InvalidStateError", + () => isCpuOptimized ? frame.getDepthInformation(view) + : glBinding.getDepthInformation(view), + "getDepthInformation() should throw when ran outside RAF"); + }); + callbackCounter--; + } + + t.step_timeout(callback, 10); + callbackCounter++; + } + + callbacksKickedOff = true; + }; + + session.requestAnimationFrame(rafCb); + + return t.step_wait(() => callbacksKickedOff && (callbackCounter == 0)); + }); + }; +}; diff --git a/testing/web-platform/tests/webxr/depth-sensing/staleViewsTests.js b/testing/web-platform/tests/webxr/depth-sensing/staleViewsTests.js new file mode 100644 index 0000000000..b1f11c9651 --- /dev/null +++ b/testing/web-platform/tests/webxr/depth-sensing/staleViewsTests.js @@ -0,0 +1,39 @@ +'use strict'; + +const staleViewsTestFunctionGenerator = function(isCpuOptimized) { + return (session, controller, t, sessionObjects) => { + let done = false; + + const staleViews = new Set(); + + return session.requestReferenceSpace('viewer').then((viewerSpace) => { + const glBinding = new XRWebGLBinding(session, sessionObjects.gl); + + const secondRafCb = function(time, frame) { + for(const view of staleViews) { + t.step(() => { + assert_throws_dom("InvalidStateError", + () => isCpuOptimized ? frame.getDepthInformation(view) + : glBinding.getDepthInformation(view), + "getDepthInformation() should throw when run with stale XRView"); + }); + } + + done = true; + }; + + const firstRafCb = function(time, frame) { + const pose = frame.getViewerPose(viewerSpace); + for(const view of pose.views) { + staleViews.add(view); + } + + session.requestAnimationFrame(secondRafCb); + }; + + session.requestAnimationFrame(firstRafCb); + + return t.step_wait(() => done); + }); + }; +};
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/dom-overlay/META.yml b/testing/web-platform/tests/webxr/dom-overlay/META.yml new file mode 100644 index 0000000000..be2b6e613a --- /dev/null +++ b/testing/web-platform/tests/webxr/dom-overlay/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/dom-overlays/ diff --git a/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay.https.html b/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay.https.html new file mode 100644 index 0000000000..250adf9b24 --- /dev/null +++ b/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay.https.html @@ -0,0 +1,298 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> + +<style type="text/css"> + div { + padding: 10px; + 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> + <!-- 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> +</div> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "alpha-blend", + interactionMode: "screen-space" +}; + +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); + + // 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_equals(session.domOverlayState, session.domOverlayState); + + // 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 = async 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. + + // Wait for a rAF call before proceeding. + await new Promise((resolve) => session.requestAnimationFrame(resolve)); + + assert_implements_optional(document.fullscreenElement == overlayElement, + "WebXR DOM overlay is not using Fullscreen API"); + let elem = document.getElementById('div_other'); + assert_not_equals(elem, null); + assert_not_equals(elem, overlayElement); + + try { + await elem.requestFullscreen(); + assert_unreached("fullscreen change should be blocked"); + } catch { + // pass if the call rejects + } + // This is an async function, its return value is automatically a promise. +}; + +let watcherStep = new Event("watcherstep"); +let watcherDone = new Event("watcherdone"); + +let testInput = function(overlayElement, session, fakeDeviceController, t) { + let debug = xr_debug.bind(this, 'testInput'); + + // 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; + }); + + 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. + debug('got viewerSpace'); + requestSkipAnimationFrame(session, (time, xrFrame) => { + debug('got rAF 1'); + input_source.setOverlayPointerPosition(inner_a.offsetLeft + 1, + inner_a.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 2'); + input_source.endSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 3'); + // Need to process one more frame to allow select to propagate. + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 4'); + session.dispatchEvent(watcherStep); + + assert_true(got_beforexrselect); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 5'); + input_source.setOverlayPointerPosition(inner_b.offsetLeft + 1, + inner_b.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 6'); + input_source.endSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 7'); + // Need to process one more frame to allow select to propagate. + session.dispatchEvent(watcherDone); + }); + }); + }); + }); + }); + }); + }); + }); + return eventPromise; +}; + +let testCrossOriginContent = function(overlayElement, session, fakeDeviceController, t) { + let debug = xr_debug.bind(this, 'testCrossOriginContent'); + + 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. + requestSkipAnimationFrame(session, (time, xrFrame) => { + debug('got rAF 1'); + input_source.setOverlayPointerPosition(iframe.offsetLeft + 1, + iframe.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 2'); + input_source.endSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 3'); + // Need to process one more frame to allow select to propagate. + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 4'); + session.dispatchEvent(watcherStep); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 5'); + input_source.setOverlayPointerPosition(inner_b.offsetLeft + 1, + inner_b.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 6'); + input_source.endSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 7'); + // Need to process one more frame to allow select to propagate. + session.dispatchEvent(watcherDone); + }); + }); + }); + }); + }); + }); + }); + }); + 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, body element", + testBasicProperties.bind(this, document.body), + fakeDeviceInitParams, 'immersive-ar', + {requiredFeatures: ['dom-overlay'], + domOverlay: { root: document.body } }); + +xr_session_promise_test( + "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') } + }); + +</script> diff --git a/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay_hit_test.https.html b/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay_hit_test.https.html new file mode 100644 index 0000000000..6fdf2498ed --- /dev/null +++ b/testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay_hit_test.https.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> + +<style type="text/css"> + div { + padding: 10px; + min-width: 10px; + min-height: 10px; + } + iframe { + border: 0; + width: 20px; + height: 20px; + } +</style> +<div id="div_overlay"> + <div id="inner_b"> + </div> + <!-- 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> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +const hitTestOptionsInit = { + profile: "generic-touchscreen", + offsetRay: new XRRay(), +}; + +const SCREEN_POINTER_TRANSFORM = { + position: [0, 0, 0], // middle of the screen + orientation: [0, 0, 0, 1] // forward-facing +}; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer + profiles: ["generic-touchscreen",] +}; + +const testCrossOriginContent = function(overlayElement, session, fakeDeviceController, t) { + const iframe = document.getElementById('iframe'); + const inner_b = document.getElementById('inner_b'); + + let debug = xr_debug.bind(this, 'testCrossOriginContent'); + + const input_source = + fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + debug('start'); + return session.requestReferenceSpace('viewer').then(function(viewerSpace) { + debug('got viewerSpace'); + return session.requestHitTestSourceForTransientInput(hitTestOptionsInit) + .then((hitTestSource) => { + debug('got hitTestSource'); + return new Promise((resolve) => { + // Press the primary input button and then release it a short time later. + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 1'); + input_source.setOverlayPointerPosition(iframe.offsetLeft + 1, + iframe.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + input_source.endSelection(); + + // There should be no results for transient input for cross origin content: + const results = xrFrame.getHitTestResultsForTransientInput(hitTestSource); + t.step(() => { + assert_equals(results.length, 0, "Hit test results should be suppressed for cross-origin content"); + }); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 2'); + // Need to process one more frame to allow select to propagate + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 3'); + input_source.setOverlayPointerPosition(inner_b.offsetLeft + 1, + inner_b.offsetTop + 1); + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + debug('got rAF 4'); + input_source.endSelection(); + + const results = xrFrame.getHitTestResultsForTransientInput(hitTestSource); + t.step(() => { + // TODO(bialpio): this assertion is currently failing, FIXME + assert_equals(results.length, 1, "Hit test results should be available for same-origin content"); + }); + debug('resolving'); + resolve(); + }); + }); + }); + }); + }); + }); + }); + }); +}; + +xr_session_promise_test( + "Ensures DOM Overlay interactions on cross origin iframe do not cause hit test results to come up", + testCrossOriginContent.bind(this, document.getElementById('div_overlay')), + fakeDeviceInitParams, 'immersive-ar', { + requiredFeatures: ['dom-overlay', 'hit-test'], + domOverlay: { root: document.getElementById('div_overlay') } + }); + +</script> diff --git a/testing/web-platform/tests/webxr/dom-overlay/idlharness.https.window.js b/testing/web-platform/tests/webxr/dom-overlay/idlharness.https.window.js new file mode 100644 index 0000000000..0f901e5b5d --- /dev/null +++ b/testing/web-platform/tests/webxr/dom-overlay/idlharness.https.window.js @@ -0,0 +1,20 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://immersive-web.github.io/dom-overlays/ + +idl_test( + ['webxr-dom-overlays'], + ['webxr', 'html', 'dom', 'SVG'], + async idl_array => { + self.svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + idl_array.add_objects({ + Document: ['document'], + HTMLElement: ['document.body'], + SVGElement: ['svgElement'], + Window: ['window'] + }); + } +); diff --git a/testing/web-platform/tests/webxr/dom-overlay/nested_fullscreen.https.html b/testing/web-platform/tests/webxr/dom-overlay/nested_fullscreen.https.html new file mode 100644 index 0000000000..a8fc70fca6 --- /dev/null +++ b/testing/web-platform/tests/webxr/dom-overlay/nested_fullscreen.https.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> + +<style type="text/css"> + div { + padding: 10px; + min-width: 10px; + min-height: 10px; + } + iframe { + border: 0; + width: 20px; + height: 20px; + } +</style> +<div id="div_overlay"> + <canvas> + </canvas> +</div> +<div id="div_other"> + <p>test text</p> +</div> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, +}; + +// This test verifies that WebXR DOM Overlay mode works when the document is +// already in fullscreen mode when the session starts. (This should work both +// for a fullscreen-based overlay implementation and for one that treats the +// overlay as an independent output.) +promise_test( + async (setup) => { + setup.add_cleanup(() => document.exitFullscreen()); + + // Fullscreen the <body> element before running the test. Currently, this + // can't be an arbitrary element because the simulateUserActivation call + // adds a button to <body> which is only clickable if it's visible. + await test_driver.bless("fullscreen", + () => document.body.requestFullscreen()); + + const overlayElement = document.getElementById('div_overlay'); + + xr_session_promise_test( + "Check XR session from fullscreen", + (session, fakeDeviceController, t) => { + // 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); + + // Wait for one animation frame before exiting. + return new Promise((resolve) => session.requestAnimationFrame(resolve)); + }, + fakeDeviceInitParams, 'immersive-ar', { + requiredFeatures: ['dom-overlay'], + domOverlay: { root: overlayElement } + } + ); + + // The setup promise_test automatically succeeds if it gets here + // without raising an exception. It'll pass even on systems that + // don't support WebXR or DOM Overlay. + }, + "fullscreen setup" +); + +</script> diff --git a/testing/web-platform/tests/webxr/events_input_source_recreation.https.html b/testing/web-platform/tests/webxr/events_input_source_recreation.https.html new file mode 100644 index 0000000000..f7ef86c273 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_input_source_recreation.https.html @@ -0,0 +1,143 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Input sources are re-created when handedness or target ray mode changes"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + let eventPromise = eventWatcher.wait_for(["watcherdone"]); + + let inputChangeEvents = 0; + let cached_input_source = null; + function onInputSourcesChange(event) { + t.step(() => { + inputChangeEvents++; + assert_equals(event.session, session); + + if (inputChangeEvents == 1) { + // The first change event should be adding our controller. + validateAdded(event.added, 1); + validateRemoved(event.removed, 0); + cached_input_source = getInputSources()[0]; + assert_not_equals(cached_input_source, null); + assert_equals(cached_input_source.handedness, "none"); + assert_equals(cached_input_source.targetRayMode, "gaze"); + assertProfilesEqual(cached_input_source.profiles, ["a", "b"]); + } else if (inputChangeEvents == 2) { + // The second event should be replacing the controller with one that has + // the updated target ray mode. + validateInputSourceChange(event, "none", "tracked-pointer", ["a", "b"]); + cached_input_source = getInputSources()[0]; + } else if (inputChangeEvents == 3) { + // The third event should be replacing the controller with one that has + // the updated handedness. + validateInputSourceChange(event, "left", "tracked-pointer", ["a", "b"]); + cached_input_source = getInputSources()[0]; + } else if (inputChangeEvents == 4) { + // The fourth event should be updating the second value in the profiles + // array. + validateInputSourceChange(event, "left", "tracked-pointer", ["a", "c"]); + cached_input_source = getInputSources()[0]; + } else if (inputChangeEvents == 5) { + // The fifth event should be removing the second value from the profiles + // array. + validateInputSourceChange(event, "left", "tracked-pointer", ["a"]); + session.dispatchEvent(watcherDone); + } + }); + } + + function assertProfilesEqual(profiles, expected_profiles) { + assert_equals(profiles.length, expected_profiles.length); + for (let i = 0; i < profiles.length; ++i) { + assert_equals(profiles[i], expected_profiles[i]); + } + } + + function validateInputSourceChange( + event, expected_hand, expected_mode, expected_profiles) { + validateAdded(event.added, 1); + validateRemoved(event.removed, 1); + assert_true(event.removed.includes(cached_input_source)); + assert_false(event.added.includes(cached_input_source)); + let source = event.added[0]; + assert_equals(source.handedness, expected_hand); + assert_equals(source.targetRayMode, expected_mode); + assertProfilesEqual(source.profiles, expected_profiles); + } + + function validateAdded(added, length) { + t.step(() => { + assert_not_equals(added, null); + assert_equals(added.length, length, + "Added length matches expectations"); + + let currentSources = getInputSources(); + added.forEach((source) => { + assert_true(currentSources.includes(source), + "Every element in added should be in the input source list"); + }); + }); + } + + function validateRemoved(removed, length) { + t.step(() => { + assert_not_equals(removed, null); + assert_equals(removed.length, length, + "Removed length matches expectations"); + + let currentSources = getInputSources(); + removed.forEach((source) => { + assert_false(currentSources.includes(source), + "No element in removed should be in the input source list"); + }); + }); + } + + function getInputSources() { + return Array.from(session.inputSources.values()); + } + + session.addEventListener('inputsourceschange', onInputSourcesChange, false); + + // Create a gaze based input source with no handedness that we can change + // to validate SameObject properties. + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "none", + targetRayMode: "gaze", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: ["a", "b"] + }); + + // Make our first input source change after one frame, and wait an additional + // frame for that change to propogate. Then make additional changes, waiting + // one frame after each one to verify that they fired an inputsourceschanged + // event. + session.requestAnimationFrame((time, xrFrame) => { + input_source.setTargetRayMode("tracked-pointer"); + session.requestAnimationFrame((time, xrFrame) => { + input_source.setHandedness("left"); + session.requestAnimationFrame((time, xrFrame) => { + input_source.setProfiles(["a", "c"]); + session.requestAnimationFrame((time, xrFrame) => { + input_source.setProfiles(["a"]); + session.requestAnimationFrame((time, xrFrame) => {}); + }); + }); + }); + }); + + return eventPromise; +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/events_input_sources_change.https.html b/testing/web-platform/tests/webxr/events_input_sources_change.https.html new file mode 100644 index 0000000000..b2a17d4e17 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_input_sources_change.https.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Transient input sources fire events in the right order"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher( + t, session, ["inputsourceschange", "selectstart", "select", "selectend", "watcherdone"]); + let eventPromise = eventWatcher.wait_for( + ["inputsourceschange", "selectstart", "select", "selectend", "inputsourceschange", "watcherdone"]); + + let inputChangeEvents = 0; + let cached_input_source = null; + function onInputSourcesChange(event) { + t.step(() => { + inputChangeEvents++; + assert_equals(event.session, session); + validateSameObject(event); + + // The first change event should be adding our controller. + if (inputChangeEvents === 1) { + validateAdded(event.added, 1); + validateRemoved(event.removed, 0); + cached_input_source = session.inputSources[0]; + assert_not_equals(cached_input_source, null); + } else if (inputChangeEvents === 2) { + // The second event should be removing our controller. + validateAdded(event.added, 0); + validateRemoved(event.removed, 1); + assert_true(event.removed.includes(cached_input_source)); + session.dispatchEvent(watcherDone); + } + }); + } + + function validateAdded(added, length) { + t.step(() => { + assert_not_equals(added, null); + assert_equals(added.length, length, + "Added length matches expectations"); + + let currentSources = Array.from(session.inputSources.values()); + added.forEach((source) => { + assert_true(currentSources.includes(source), + "Every element in added should be in the input source list"); + }); + }); + } + + function validateRemoved(removed, length) { + t.step(() => { + assert_not_equals(removed, null); + assert_equals(removed.length, length, + "Removed length matches expectations"); + + let currentSources = Array.from(session.inputSources.values()); + removed.forEach((source) => { + assert_false(currentSources.includes(source), + "No element in removed should be in the input source list"); + }); + }); + } + + // Verifies that the same object is returned each time attributes are accessed + // on an XRInputSourcesChangeEvent, as required by the spec. + function validateSameObject(event) { + let eventSession = event.session; + let added = event.added; + let removed = event.removed; + + t.step(() => { + assert_equals(eventSession, event.session, + "XRInputSourcesChangeEvent.session returns the same object."); + assert_equals(added, event.added, + "XRInputSourcesChangeEvent.added returns the same object."); + assert_equals(removed, event.removed, + "XRInputSourcesChangeEvent.removed returns the same object."); + }); + } + + session.addEventListener('inputsourceschange', onInputSourcesChange, false); + + // Create our input source and immediately toggle the primary input so that + // it appears as already needing to send a click event when it appears. + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [], + selectionClicked: true + }); + + // Make our input source disappear after one frame, and wait an additional + // frame for that disappearance to propogate. + requestSkipAnimationFrame(session, (time, xrFrame) => { + input_source.disconnect(); + session.requestAnimationFrame((time, xrFrame) => {}); + }); + + return eventPromise; +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/events_referenceSpace_reset_immersive.https.html b/testing/web-platform/tests/webxr/events_referenceSpace_reset_immersive.https.html new file mode 100644 index 0000000000..d12d8f7737 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_referenceSpace_reset_immersive.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let immersiveTestName = "XRSession resetpose from a device properly fires off " + + "the right events for immersive sessions"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let resetPromise = session.requestReferenceSpace('local') + .then((refSpace) => { + let eventWatcher = new EventWatcher( + t, refSpace, ["reset", "watcherdone"]); + refSpace.addEventListener("reset", (event) => { + t.step(() => { + assert_equals(event.referenceSpace, refSpace); + + // Also make sure the same objects are returned each time these + // attributes are accessed. + let eventRefSpace = event.referenceSpace; + let transform = event.transform; + assert_equals(eventRefSpace, event.referenceSpace, + "XRReferenceSpaceEvent.referenceSpace returns the same object."); + assert_equals(transform, event.transform, + "XRReferenceSpaceEvent.transform returns the same object."); + }); + + refSpace.dispatchEvent(watcherDone); + }, false); + return eventWatcher.wait_for(["reset", "watcherdone"]); + }); + + fakeDeviceController.simulateResetPose(); + + // The triggered resetPose event should arrive after the next Animation Frame + requestSkipAnimationFrame(session, () => {}); + + return resetPromise; +}; + +xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/events_referenceSpace_reset_inline.https.html b/testing/web-platform/tests/webxr/events_referenceSpace_reset_inline.https.html new file mode 100644 index 0000000000..a445886a52 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_referenceSpace_reset_inline.https.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let nonImmersiveTestName = "XRSession resetpose from a device properly fires off " + + "the right events for non-immersive sessions"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let resetPromise = session.requestReferenceSpace('local') + .then((refSpace) => { + let eventWatcher = new EventWatcher( + t, refSpace, ["reset", "watcherdone"]); + refSpace.addEventListener("reset", (event) => { + t.step(() => { + assert_equals(event.referenceSpace, refSpace); + + // Also make sure the same objects are returned each time these + // attributes are accessed. + let eventRefSpace = event.referenceSpace; + let transform = event.transform; + assert_equals(eventRefSpace, event.referenceSpace, + "XRReferenceSpaceEvent.referenceSpace returns the same object."); + assert_equals(transform, event.transform, + "XRReferenceSpaceEvent.transform returns the same object."); + }); + + refSpace.dispatchEvent(watcherDone); + }, false); + return eventWatcher.wait_for(["reset", "watcherdone"]); + }); + + fakeDeviceController.simulateResetPose(); + + // The triggered resetPose event should arrive after the next Animation Frame + session.requestAnimationFrame(() => {}); + + return resetPromise; +}; + +xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline', { + requiredFeatures: ['local'], + }); + +</script> diff --git a/testing/web-platform/tests/webxr/events_session_select.https.html b/testing/web-platform/tests/webxr/events_session_select.https.html new file mode 100644 index 0000000000..9b7402d2b6 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_session_select.https.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRInputSources primary input presses properly fires off the " + + "right events"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let xrViewerSpace = null; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher( + t, session, ["selectstart", "select", "selectend", "watcherdone"]); + let eventPromise = eventWatcher.wait_for( + ["selectstart", "select", "selectend", "watcherdone"]); + + function tryCallingFrameMethods(frame) { + t.step(() => { + // Frame is active but not an animation frame, so calling getPose is + // allowed while getViewerPose is not. + assert_throws_dom('InvalidStateError', () => frame.getViewerPose(currentReferenceSpace)); + let pose = frame.getPose(xrViewerSpace, currentReferenceSpace); + assert_not_equals(pose, null); + assert_true(pose instanceof XRPose); + assert_false(pose instanceof XRViewerPose); + }); + } + + function onSessionSelectStart(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + } + + function onSessionSelectEnd(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + session.dispatchEvent(watcherDone); + } + + function onSessionSelect(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + } + + // Verifies that the same object is returned each time attributes are accessed + // on an XRInputSoruceEvent, as required by the spec. + function validateSameObject(event) { + let frame = event.frame; + let source = event.inputSource; + t.step(() => { + assert_equals(frame, event.frame, + "XRInputSourceEvent.frame returns the same object."); + assert_equals(source, event.inputSource, + "XRInputSourceEvent.inputSource returns the same object."); + }); + } + + session.addEventListener("selectstart", onSessionSelectStart, false); + session.addEventListener("selectend", onSessionSelectEnd, false); + session.addEventListener("select", onSessionSelect, false); + + let input_source = + fakeDeviceController.simulateInputSourceConnection(VALID_CONTROLLER); + let currentReferenceSpace = null; + + session.requestReferenceSpace('viewer').then(function(viewerSpace) { + xrViewerSpace = viewerSpace; + + session.requestReferenceSpace('local').then((referenceSpace) => { + currentReferenceSpace = referenceSpace; + + // Press the primary input button and then release it a short time later. + requestSkipAnimationFrame(session, (time, xrFrame) => { + input_source.startSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + input_source.endSelection(); + + session.requestAnimationFrame((time, xrFrame) => { + // Need to process one more frame to allow select to propegate. + }); + }); + }); + }); + }); + + return eventPromise; +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/events_session_select_subframe.https.html b/testing/web-platform/tests/webxr/events_session_select_subframe.https.html new file mode 100644 index 0000000000..b7615c3d0b --- /dev/null +++ b/testing/web-platform/tests/webxr/events_session_select_subframe.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Ensures that an XRInputSources primary input being pressed and " + + "released in the space of a single frame properly fires off the right " + + "events"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher( + t, session, ["selectstart", "selectend", "select", "watcherdone"]); + let eventPromise = eventWatcher.wait_for( + ["selectstart", "selectend", "select", "watcherdone"]); + + function onSessionSelectStart(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + }); + } + + function onSessionSelectEnd(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + }); + } + + function onSessionSelect(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + }); + session.dispatchEvent(watcherDone); + } + session.addEventListener("selectstart", onSessionSelectStart, false); + session.addEventListener("selectend", onSessionSelectEnd, false); + session.addEventListener("select", onSessionSelect, false); + + let input_source = fakeDeviceController.simulateInputSourceConnection(VALID_CONTROLLER); + + // Press the primary input button and then release it a short time later. + requestSkipAnimationFrame(session, (time, xrFrame) => { + input_source.simulateSelect(); + + session.requestAnimationFrame((time, xrFrame) => { + // Need to process one more frame to allow select to propegate. + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/events_session_squeeze.https.html b/testing/web-platform/tests/webxr/events_session_squeeze.https.html new file mode 100644 index 0000000000..122fe340a2 --- /dev/null +++ b/testing/web-platform/tests/webxr/events_session_squeeze.https.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRInputSources primary input presses properly fires off the " + + "right events"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let xrViewerSpace = null; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher( + t, session, ["squeezestart", "squeeze", "squeezeend", "watcherdone"]); + let eventPromise = eventWatcher.wait_for( + ["squeezestart", "squeeze", "squeezeend", "watcherdone"]); + + function tryCallingFrameMethods(frame) { + t.step(() => { + // Frame is active but not an animation frame, so calling getPose is + // allowed while getViewerPose is not. + assert_throws_dom('InvalidStateError', () => frame.getViewerPose(currentReferenceSpace)); + let pose = frame.getPose(xrViewerSpace, currentReferenceSpace); + assert_not_equals(pose, null); + assert_true(pose instanceof XRPose); + assert_false(pose instanceof XRViewerPose); + }); + } + + function onSessionSqueezeStart(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + } + + function onSessionSqueezeEnd(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + session.dispatchEvent(watcherDone); + } + + function onSessionSqueeze(event) { + t.step( () => { + let input_sources = session.inputSources; + assert_equals(event.frame.session, session); + assert_equals(event.inputSource, input_sources[0]); + validateSameObject(event); + tryCallingFrameMethods(event.frame); + }); + } + + // Verifies that the same object is returned each time attributes are accessed + // on an XRInputSoruceEvent, as required by the spec. + function validateSameObject(event) { + let frame = event.frame; + let source = event.inputSource; + t.step(() => { + assert_equals(frame, event.frame, + "XRInputSourceEvent.frame returns the same object."); + assert_equals(source, event.inputSource, + "XRInputSourceEvent.inputSource returns the same object."); + }); + } + + session.addEventListener("squeezestart", onSessionSqueezeStart, false); + session.addEventListener("squeezeend", onSessionSqueezeEnd, false); + session.addEventListener("squeeze", onSessionSqueeze, false); + + let pressed_grip_button = { + buttonType: "grip", + pressed: true, + touched: true, + pressedValue: 1.0 + }; + + let unpressed_grip_button = { + buttonType: "grip", + pressed: false, + touched: false, + pressedValue: 0.0 + }; + + let gripController = { + handedness: "none", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [], + supportedButtons: [ unpressed_grip_button ] + }; + + let input_source = + fakeDeviceController.simulateInputSourceConnection(gripController); + let currentReferenceSpace = null; + + session.requestReferenceSpace('viewer').then(function(viewerSpace) { + xrViewerSpace = viewerSpace; + + session.requestReferenceSpace('local').then((referenceSpace) => { + currentReferenceSpace = referenceSpace; + + //Simulate a grip starting then release it a short time later. + requestSkipAnimationFrame(session, (time, xrFrame) => { + input_source.updateButtonState(pressed_grip_button); + + session.requestAnimationFrame((time, xrFrame) => { + input_source.updateButtonState(unpressed_grip_button); + + session.requestAnimationFrame((time, xrFrame) => { + // Need to process one more frame to allow grip to propegate. + }); + }); + }); + }); + }); + + return eventPromise; +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/exclusive_requestFrame_nolayer.https.html b/testing/web-platform/tests/webxr/exclusive_requestFrame_nolayer.https.html new file mode 100644 index 0000000000..c1c52f0374 --- /dev/null +++ b/testing/web-platform/tests/webxr/exclusive_requestFrame_nolayer.https.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let immersiveTestName = "XRSession requestAnimationFrame must fail if the session has " + + "no baseLayer for immersive"; + +let nonImmersiveTestName = "XRSession requestAnimationFrame must fail if the session has " + + "no baseLayer for non immersive"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = (session, controller, t, sessionObjects) => new Promise((resolve, reject) => { + // Clear the base layer that the boilerplate sets. This ensures that the rAF + // won't fire. If the fire *does* fire, it will fail because the baseLayer + // won't match the new baselayer we later create. + session.updateRenderState({ + baseLayer: null + }); + let gl = sessionObjects.gl; + + // Session must have a baseLayer or frame requests will be ignored. + let webglLayer = new XRWebGLLayer(session, gl); + + function onEarlyFrame(time, vrFrame) { + // We shouldn't be allowed to reach this callback with no baseLayer + t.step(() => { + assert_equals(session.renderState.baseLayer, webglLayer); + }); + resolve(); + } + + // This callback shouldn't go through, since the session doesn't + // have a baseLayer when this call is made. + let handle = session.requestAnimationFrame(onEarlyFrame); + // Should still give us a valid handle, though. + assert_not_equals(handle, 0); + + // Wait for a bit and set the baseLayer. + t.step_timeout(() => { + // Once the base layer is set the previously registered callback should run. + session.updateRenderState({ + baseLayer: webglLayer + }); + }, 300); +}); + +xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/gamepads-module/META.yml b/testing/web-platform/tests/webxr/gamepads-module/META.yml new file mode 100644 index 0000000000..a6751571a7 --- /dev/null +++ b/testing/web-platform/tests/webxr/gamepads-module/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/webxr-gamepads-module/ diff --git a/testing/web-platform/tests/webxr/gamepads-module/idlharness.https.window.js b/testing/web-platform/tests/webxr/gamepads-module/idlharness.https.window.js new file mode 100644 index 0000000000..4509c67a84 --- /dev/null +++ b/testing/web-platform/tests/webxr/gamepads-module/idlharness.https.window.js @@ -0,0 +1,16 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://immersive-web.github.io/webxr-gamepads-module/ + +idl_test( + ['webxr-gamepads-module'], + ['webxr', 'dom'], + async idl_array => { + idl_array.add_objects({ + // TODO: XRInputSource + }); + } +); diff --git a/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_disconnect.https.html b/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_disconnect.https.html new file mode 100644 index 0000000000..bd69649d44 --- /dev/null +++ b/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_disconnect.https.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> + +<script> +let testName = "WebXR InputSource's gamepad gets disconnected when the input source is removed"; + +let watcherDone = new Event("watcherdone"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher(t, session, ["watcherdone"]); + let eventPromise = eventWatcher.wait_for(["watcherdone"]); + + let inputChangeEvents = 0; + let cached_input_source = null; + function onInputSourcesChange(event) { + t.step(() => { + inputChangeEvents++; + + // The first change event should be adding our controller/gamepad. + if (inputChangeEvents === 1) { + // We should have one input source + assert_equals(session.inputSources.length, 1, + "should initially have an input source"); + assertGamepadConnected(); + } else if (inputChangeEvents === 2) { + // The second event should be disconnecting our gamepad, we should still + // have an input source. + assert_equals(session.inputSources.length, 1, + "removing the gamepad shouldn't remove the input source"); + // However, disconnecting the gamepad from the input source should cause + // the input source to be re-created. Verify this. + assertInputSourceRecreated(event); + assertGamepadDisconnected(); + cached_input_source = session.inputSources[0]; + } else if (inputChangeEvents === 3) { + assert_equals(session.inputSources.length, 1, + "re-adding the gamepad shouldn't add an extra input source"); + // The third event should be reconnecting our gamepad, we should still + // have an input source. However, it should have been re-created. + assertInputSourceRecreated(event); + assertGamepadConnected(); + } else if (inputChangeEvents === 4) { + // The fourth event should be disconnecting our gamepad, we should no + // longer have an input source. + assert_equals(session.inputSources.length, 0, + "input source should have been disconnected"); + assertGamepadDisconnected(); + } else if (inputChangeEvents === 5) { + // The fifth event should be re-connecting our gamepad to prep for + // ending the session. + assert_equals(session.inputSources.length, 1, + "input source should have been re-connected"); + assertGamepadConnected(); + session.dispatchEvent(watcherDone); + } + }); + } + + function assertInputSourceRecreated(event) { + assert_equals(event.added.length, 1); + assert_equals(event.removed.length, 1); + assert_equals(session.inputSources[0], event.added[0]); + assert_equals(cached_input_source, event.removed[0]); + } + + function assertGamepadConnected() { + cached_input_source = session.inputSources[0]; + assert_not_equals(cached_input_source, null, + "Expect to get a cached_input_source, iteration: " + inputChangeEvents); + assert_not_equals(cached_input_source.gamepad, null, + "Expect to have a gamepad, iteration: " + inputChangeEvents); + assert_equals(cached_input_source.gamepad.index, -1, + "WebXR Gamepad.index must be -1, iteration: " + inputChangeEvents); + assert_equals(cached_input_source.gamepad.id, "", + "WebXR Gamepad.id must be empty string, iteration: " + inputChangeEvents); + assert_true(cached_input_source.gamepad.connected, + "Expect the gamepad to be connected, iteration: " + inputChangeEvents); + } + + function assertGamepadDisconnected() { + assert_not_equals(cached_input_source, null, + "Expect to have a cached_input_source, iteration: " + inputChangeEvents); + assert_not_equals(cached_input_source.gamepad, null, + "Expect to have a gamepad on cached_input_source, iteration: " + inputChangeEvents); + assert_equals(cached_input_source.gamepad.index, -1, + "WebXR Gamepad.index must be -1, iteration: " + inputChangeEvents); + assert_equals(cached_input_source.gamepad.id, "", + "WebXR Gamepad.id must be empty string, iteration: " + inputChangeEvents); + assert_false(cached_input_source.gamepad.connected, + "Expect cached gamepad to be disconnected, iteration: " + inputChangeEvents); + } + + session.addEventListener('inputsourceschange', onInputSourcesChange, false); + + // A set of supported buttons which should cause the runtime to treat the + // controller as supporting a gamepad. + let gamepadButtons = [ + { + buttonType: 'grip', + pressed: false, + touched: false, + pressedValue: 0 + }, + { + buttonType: 'touchpad', + pressed: false, + touched: false, + pressedValue: 0 + } + ]; + + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [], + supportedButtons: gamepadButtons + }); + + // Input events need one frame to propagate, so this does (in order and running + // a rAF after each step: + // 1. Disconnect the gamepad (so we can verify that the gamepad is disconnected) + // 2. Reconnect the gamepad (so we can set up to disconnect the controller) + // 3. Disconnect the controller (so we can verify that it's gamepad gets disconnected). + // 4. Adds the controller back (so we can test the end Session) + // 5. Waits for all of the input events to finish propagating, then ends the + // session, at which point the controller should be disconnected. + return new Promise((resolve) => { + requestSkipAnimationFrame(session, () => { + input_source.setSupportedButtons([]); + session.requestAnimationFrame(() => { + input_source.setSupportedButtons(gamepadButtons); + session.requestAnimationFrame(() => { + input_source.disconnect(); + session.requestAnimationFrame(() => { + input_source.reconnect(); + session.requestAnimationFrame(() => { + eventPromise.then(() => { + session.end().then(() => { + assertGamepadDisconnected(); + resolve(); + }); + }); + }); + }); + }); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_input_registered.https.html b/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_input_registered.https.html new file mode 100644 index 0000000000..28f3084671 --- /dev/null +++ b/testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_input_registered.https.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> + +<script> +let testName = "WebXR InputSource's gamepad properly registers input"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + + // There should only be one input source change event, which is from adding + // the input source at the start of the test. + let inputChangeEvents = 0; + function onInputSourcesChange(event) { + assert_equals(inputChangeEvents, 0, + "Gamepad button or input axis value changes should not fire an input source change event."); + inputChangeEvents++; + } + + session.addEventListener('inputsourceschange', onInputSourcesChange, false); + + // Create our input source and immediately toggle the primary input so that + // it appears as already needing to send a click event when it appears. + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [], + supportedButtons: [ + { + buttonType: 'grip', + pressed: false, + touched: false, + pressedValue: 0 + }, + { + buttonType: 'touchpad', + pressed: false, + touched: false, + pressedValue: 0 + } + ] + }); + + let cached_input_source = null; + let cached_gamepad = null; + + function assertSameObjects() { + assert_equals(session.inputSources[0], cached_input_source); + assert_equals(cached_input_source.gamepad, cached_gamepad); + + // Also make sure that WebXR gamepads have the index and id values required + // by the spec. + assert_equals(cached_gamepad.index, -1); + assert_equals(cached_gamepad.id, ""); + } + + // Input events and gamepad state changes (button presses, axis movements) + // need one frame to propagate, so this does (in order and running a rAF after + // each step): + // 1) Press the mock gamepad's button (so we can verify the button press makes + // its way to the WebXR gamepad and that it does not fire an + // inputsourceschange event). + // 2) Update the mock gamepad's input axes values (so we can verify the + // updated values make their way to the WebXR gamepad and that it does not + // fire an inputsourceschange event). + return new Promise((resolve) => { + requestSkipAnimationFrame(session, () => { + // Make sure the exposed gamepad has the number of buttons and axes we + // requested. + // 3 Buttons: trigger, grip, touchpad + // 2 Axes from the touchpad + cached_input_source = session.inputSources[0]; + cached_gamepad = cached_input_source.gamepad; + t.step(() => { + assert_equals(cached_gamepad.index, -1); + assert_equals(cached_gamepad.id, ""); + assert_equals(cached_gamepad.buttons.length, 3); + assert_equals(cached_gamepad.axes.length, 2); + // Initially, the button should not be pressed and the axes values should + // be set to 0. + assert_false(cached_gamepad.buttons[1].pressed); + assert_equals(cached_gamepad.axes[0], 0); + assert_equals(cached_gamepad.axes[1], 0); + }, "Verify initial state"); + // Simulate button press. + input_source.updateButtonState({ + buttonType: 'grip', + pressed: true, + touched: true, + value: 1.0 + }); + session.requestAnimationFrame(() => { + t.step(() => { + assertSameObjects(); + assert_true(cached_gamepad.buttons[1].pressed); + }, "Gamepad is updated in place when a button is pressed"); + + // Simulate input axes movement. + input_source.updateButtonState({ + buttonType: 'touchpad', + pressed: false, + touched: true, + value: 0, + xValue: 0.5, + yValue: -0.5 + }); + session.requestAnimationFrame(() => { + // Input source and gamepad should not be re-created. They should be + // updated in place when input axes values change. + t.step(() => { + assertSameObjects(); + assert_equals(cached_gamepad.axes[0], 0.5); + assert_equals(cached_gamepad.axes[1], -0.5); + // Button that was pressed last frame should still be pressed. + assert_true(cached_gamepad.buttons[1].pressed); + }, "Gamepad is updated in place when axes values change"); + resolve(); + }); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/getInputPose_handedness.https.html b/testing/web-platform/tests/webxr/getInputPose_handedness.https.html new file mode 100644 index 0000000000..9dbaf3b629 --- /dev/null +++ b/testing/web-platform/tests/webxr/getInputPose_handedness.https.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +let testName = "XRInputSources properly communicate their handedness"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "none", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] + }); + + function CheckNone(time, xrFrame) { + let source = session.inputSources[0]; + + t.step( () => { + assert_not_equals(source, null); + // Handedness should be "none" by default. + assert_equals(source.handedness, "none"); + }); + + input_source.setHandedness("right"); + + session.requestAnimationFrame(CheckRight); + } + + function CheckRight(time, xrFrame) { + let source = session.inputSources[0]; + + t.step( () => { + assert_not_equals(source, null); + // Handedness was set to "right", make sure it propegates. + assert_equals(source.handedness, "right"); + }); + + input_source.setHandedness("left"); + + session.requestAnimationFrame(CheckLeft); + } + + function CheckLeft(time, xrFrame) { + let source = session.inputSources[0]; + + t.step( () => { + assert_not_equals(source, null); + // Handedness was set to "left", make sure it propegates. + assert_equals(source.handedness, "left"); + }); + + input_source.setHandedness("none"); + + session.requestAnimationFrame(CheckNoneAgain); + } + + function CheckNoneAgain(time, xrFrame) { + let source = session.inputSources[0]; + + t.step( () => { + assert_not_equals(source, null); + // Handedness was set to "none" again, make sure it propegates. + assert_equals(source.handedness, "none"); + }); + + resolve(); + } + + // Handedness only updates during an XR frame. + requestSkipAnimationFrame(session, CheckNone); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/getInputPose_pointer.https.html b/testing/web-platform/tests/webxr/getInputPose_pointer.https.html new file mode 100644 index 0000000000..302dc399ee --- /dev/null +++ b/testing/web-platform/tests/webxr/getInputPose_pointer.https.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let testName = "XRInputSources with a target ray mode of 'tracked-pointer' " + + "properly communicate their poses"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: IDENTITY_TRANSFORM, + profiles: [] + }); + + // Don't set a grip matrix yet + + // Must have a reference space to get input poses. eye-level doesn't apply + // any transforms to the given matrix. + session.requestReferenceSpace('local').then( (referenceSpace) => { + + function CheckInvalidGrip(time, xrFrame) { + let source = session.inputSources[0]; + let grip_pose = xrFrame.getPose(source.gripSpace, referenceSpace); + + t.step( () => { + // The input pose should be null when no grip matrix is provided. + assert_equals(source.targetRayMode, "tracked-pointer"); + assert_equals(grip_pose, null); + }); + + input_source.setGripOrigin(VALID_GRIP_TRANSFORM); + + session.requestAnimationFrame(CheckValidGrip); + } + + function CheckValidGrip(time, xrFrame) { + let source = session.inputSources[0]; + + let grip_pose = xrFrame.getPose(source.gripSpace, referenceSpace); + + let input_pose = xrFrame.getPose(source.targetRaySpace, referenceSpace); + + t.step( () => { + // When a grip matrix is set, both the grip and pointer matrices + // should yield their set values (i.e. the pointerOrigin is *not* + // transformed by the gripOrigin). + assert_not_equals(grip_pose, null); + assert_matrix_approx_equals(grip_pose.transform.matrix, VALID_GRIP, + FLOAT_EPSILON, "Grip matrix is not equal to input."); + assert_matrix_approx_equals(input_pose.transform.matrix, + IDENTITY_MATRIX, FLOAT_EPSILON, + "Pointer matrix is not equal to its set value."); + }); + + input_source.setPointerOrigin(VALID_POINTER_TRANSFORM); + + session.requestAnimationFrame(CheckValidGripAndPointer); + } + + function CheckValidGripAndPointer(time, xrFrame) { + let source = session.inputSources[0]; + + let grip_pose = xrFrame.getPose(source.gripSpace, referenceSpace); + let input_pose = xrFrame.getPose(source.targetRaySpace, referenceSpace); + + t.step( () => { + // Verify that changes to the pointer origin are properly reflected. + assert_not_equals(grip_pose, null); + assert_matrix_approx_equals(grip_pose.transform.matrix, VALID_GRIP, + FLOAT_EPSILON, "Grip matrix is not equal to input valid grip."); + assert_matrix_approx_equals(input_pose.transform.matrix, + VALID_POINTER, FLOAT_EPSILON, + "Pointer matrix not set properly."); + }); + + resolve(); + } + + // Can only request input poses in an xr frame. + requestSkipAnimationFrame(session, CheckInvalidGrip); + }); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/getViewerPose_emulatedPosition.https.html b/testing/web-platform/tests/webxr/getViewerPose_emulatedPosition.https.html new file mode 100644 index 0000000000..7bffaf51a5 --- /dev/null +++ b/testing/web-platform/tests/webxr/getViewerPose_emulatedPosition.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + + let testName = "XRFrame getViewerPose has emulatedPosition set properly."; + + const poseTransform = { + position: [1, 1, 1], + orientation: [0.5, 0.5, 0.5, 0.5] + }; + + let testFunction = function(session, fakeDeviceController, t) { + let debug = xr_debug.bind(this, 'testFunction'); + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve, reject) => { + debug('refSpace promise'); + function CheckPositionNotEmulated(time, vrFrame){ + debug('rAF 1: checkPositionNotEmulated'); + t.step(() => { + let pose = vrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + assert_equals(pose.emulatedPosition, false); + fakeDeviceController.setViewerOrigin(poseTransform, true); + }); + + session.requestAnimationFrame(CheckPositionEmulated); + } + + function CheckPositionEmulated(time, vrFrame) { + debug('rAF 2: checkPositionEmulated'); + t.step(() => { + let pose = vrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + assert_equals(pose.emulatedPosition, true); + }); + + // Finished. + debug('resolve'); + resolve(); + } + + requestSkipAnimationFrame(session, CheckPositionNotEmulated); + })); + }; + + xr_session_promise_test(testName, testFunction, + TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/hand-input/META.yml b/testing/web-platform/tests/webxr/hand-input/META.yml new file mode 100644 index 0000000000..caa72f8632 --- /dev/null +++ b/testing/web-platform/tests/webxr/hand-input/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/webxr-hand-input/ diff --git a/testing/web-platform/tests/webxr/hand-input/idlharness.https.window.js b/testing/web-platform/tests/webxr/hand-input/idlharness.https.window.js new file mode 100644 index 0000000000..184ec6b306 --- /dev/null +++ b/testing/web-platform/tests/webxr/hand-input/idlharness.https.window.js @@ -0,0 +1,16 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://immersive-web.github.io/webxr-hand-input/ + +idl_test( + ['webxr-hand-input'], + ['webxr', 'dom'], + async idl_array => { + idl_array.add_objects({ + // TODO + }); + } +); diff --git a/testing/web-platform/tests/webxr/hit-test/META.yml b/testing/web-platform/tests/webxr/hit-test/META.yml new file mode 100644 index 0000000000..e3f94f05a1 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/hit-test/ diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html new file mode 100644 index 0000000000..5e7381df02 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// at world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +// at world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +const test_function_generator = function(isTransientTest, isSessionEndedTest) { + return function(session, fakeDeviceController, t) { + + let done = false; + + return session.requestReferenceSpace('local').then((localSpace) => { + const validation_function = (hitTestSource) => { + + const rAFcb = function(time, frame) { + if(isSessionEndedTest) { + // Session is marked as "ended" synchronously, there is no need to + // wait for the promise it returns to settle. + session.end(); + + // Currently, the specification does not say what happen + // when a hit test source gets cancelled post-session-end. + hitTestSource.cancel(); + done = true; + } else { + hitTestSource.cancel(); + t.step(() => { + assert_throws_dom("InvalidStateError", () => hitTestSource.cancel()); + }); + done = true; + } + }; + + session.requestAnimationFrame(rAFcb); + + return t.step_wait(() => done); + }; + + // Same validation will happen both in transient and non-transient variant + if(isTransientTest) { + return session.requestHitTestSourceForTransientInput({ + profile: "generic-touchscreen", + offsetRay: new XRRay(), + }).then(validation_function); + } else { + return session.requestHitTestSource({ + space: localSpace, + offsetRay: new XRRay(), + }).then(validation_function); + } + }); // return session.requestReferenceSpace('local').then((localSpace) => { ... + }; // return function(session, fakeDeviceController, t) { ... +} + +xr_session_promise_test("Ensures hit test source cancellation works when the session has not ended.", + test_function_generator(/*isTransientTest=*/false, /*isSessionEndedTest=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures transient input hit test source cancellation works when the session has not ended.", + test_function_generator(/*isTransientTest=*/true, /*isSessionEndedTest=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures hit test source cancellation works when the session has ended", + test_function_generator(/*isTransientTest=*/false, /*isSessionEndedTest=*/true), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Ensures transient input hit test source cancellation works when the session has ended", + test_function_generator(/*isTransientTest=*/true, /*isSessionEndedTest=*/true), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html new file mode 100644 index 0000000000..ca92d390fc --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html @@ -0,0 +1,170 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, -0.25, 0], + orientation: [0, 0, 0, 1], +}; + +// Start the screen pointer at the same place as the viewer, so it's essentially +// coming straight forward from the middle of the screen. +const SCREEN_POINTER_TRANSFORM = VIEWER_ORIGIN_TRANSFORM; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer + profiles: ["generic-touchscreen",] +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +// Generates a test function given the parameters for the hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |inputFromPointer| - input from pointer transform that will be used as the input source's +// inputFromPointer (aka pointer origin) in subsequent rAF. +// |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { + const testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + + requestSkipAnimationFrame(session, (time, frame) => { + t.step(() => { + assert_equals(session.inputSources.length, 1); + }); + + const input_source = session.inputSources[0]; + const hitTestOptionsInit = { + space: input_source.targetRaySpace, + offsetRay: ray, + }; + + session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { + t.step(() => { + assert_not_equals(hitTestSource, null); + }); + + // We got a hit test source, now get the results in subsequent rAFcb: + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, expectedPoses.length); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); + } + } + }); + + input_source_controller.setPointerOrigin(inputFromPointer, false); + + session.requestAnimationFrame((time, frame) => { + const results = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(results.length, nextFrameExpectedPoses.length); + for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { + const pose = results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); + } + } + }); + + resolve(); + }); + }); + }); + }); + })); + }; + + return testFunction; +}; + + +// Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - no move", + testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const moved_pointer_transform_1 = { + position: [0, 1, 0], + orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - no results", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const pose_2 = { + position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, + // Intersection point will be on the same height as the viewer, on the front + // wall. Distance from the front wall to viewer is 2.5m, and we are rotating + // to the left, so X coordinate of the intersection point will be negative + // & equal to -2.5 * tan(30 deg) ~= 1.443m. + orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y axis facing towards world's + // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis + // towards world's Y axis ([0,1,0]). + // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. +}; + +const moved_pointer_transform_2 = { + position: [0, 1, 0], + orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, + // creating 30-60-90 triangle with the front wall +}; + +xr_session_promise_test("Ensures subscription to hit test works with an XRSpace from input source - after move - 1 result", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html new file mode 100644 index 0000000000..a30e71949c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html @@ -0,0 +1,158 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0.25, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +// Generates a test function given the parameters for the hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses are expected to be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |refSpaceName| - XRReferenceSpaceType - either 'local', 'local-floor' or 'viewer'. +let testFunctionGenerator = function(ray, expectedPoses, refSpaceName) { + const testFunction = function(session, fakeDeviceController, t) { + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + session.requestReferenceSpace('local-floor'), + ]).then(([localRefSpace, viewerRefSpace, localFloorRefSpace]) => { + + const refSpaceNameToSpace = { + 'local' : localRefSpace, + 'viewer' : viewerRefSpace, + 'local-floor' : localFloorRefSpace + }; + + const hitTestOptionsInit = { + space: refSpaceNameToSpace[refSpaceName], + offsetRay: ray, + }; + + return session.requestHitTestSource(hitTestOptionsInit).then( + (hitTestSource) => new Promise((resolve, reject) => { + + const requestAnimationFrameCallback = function(time, frame) { + const hitTestResults = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_equals(hitTestResults.length, expectedPoses.length, "Results length should match expected results length"); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = hitTestResults[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose); + } + } + }); + + resolve(); + }; + + t.step(() => { + assert_true(hitTestSource != null, "Hit test source should not be null"); + }); + + session.requestAnimationFrame(requestAnimationFrameCallback); + })); + }); + }; + + return testFunction; +}; + +// Generates a test function that will use local space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const localBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'local'); +}; + +// Generates a test function that will use viewer space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const viewerBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'viewer'); +}; + +// Generates a test function that will use local-floor space for hit test subscription. +// See testFunctionGenerator for explanation of other parameters. +const localFloorBasedTestFunctionGenerator = function(ray, expectedPoses) { + return testFunctionGenerator(ray, expectedPoses, 'local-floor'); +}; + +// All test cases require local-floor and hit-test. +const sessionInit = { 'requiredFeatures': ['local-floor', 'hit-test'] }; + +// Pose of the first expected hit test result - straight ahead of the viewer, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test( + "Ensures subscription to hit test works with viewer space - straight ahead - plane", + viewerBasedTestFunctionGenerator(new XRRay(), [pose_1]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +xr_session_promise_test("Ensures subscription to hit test works with viewer space - straight up - no results", + viewerBasedTestFunctionGenerator(new XRRay({}, {x: 0.0, y: 1.0, z : 0.0}), []), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +const pose_2 = { + position: {x: 0.0, y: 0.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y and Z axis towards the ray origin so it won't be used, + // but `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with local space", + localBasedTestFunctionGenerator(new XRRay(), [pose_2]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +const pose_3 = { + position: {x: 0.0, y: 0.25, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y and Z axis towards the ray origin so it won't be used, + // but `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to hit test works with local-floor space", + localFloorBasedTestFunctionGenerator(new XRRay(), [pose_3]), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html new file mode 100644 index 0000000000..9f5513fd1e --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +// Creates a test method that leverages regular hit test API (as opposed to hit +// test for transient input). +// |shouldSucceed| - true if the hit test request is expected to succeed, false otherwise +// |endSession| - true if the test case should call session.end() prior to requesting hit test +// |expectedError| - expected error name that should be returned in case shouldSucceed is false +const testFunctionGeneratorRegular = function(shouldSucceed, endSession, expectedError) { + const testFunction = function(session, fakeDeviceController, t) { + session.requestReferenceSpace('viewer').then((viewerRefSpace) => { + + const hitTestOptionsInit = { + space: viewerRefSpace, + offsetRay: new XRRay(), + }; + + if(endSession) { + session.end(); + } + + return session.requestHitTestSource(hitTestOptionsInit).then((hitTestSource) => { + t.step(() => { + assert_true(shouldSucceed, + "`requestHitTestSource` succeeded when it was expected to fail"); + }); + }).catch((error) => { + t.step(() => { + assert_false(shouldSucceed, + "`requestHitTestSource` failed when it was expected to succeed, error: " + error); + assert_equals(error.name, expectedError, + "`requestHitTestSource` failed with unexpected error name"); + }); + }); + }); + }; + + return testFunction; +}; + +xr_session_promise_test("Hit test subscription succeeds if the feature was requested", + testFunctionGeneratorRegular(/*shouldSucceed=*/true, /*endSession=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Hit test subscription fails if the feature was not requested", + testFunctionGeneratorRegular(/*shouldSucceed=*/false, /*endSession=*/false, "NotSupportedError"), + fakeDeviceInitParams, + 'immersive-ar', {}); + +xr_session_promise_test("Hit test subscription fails if the feature was requested but the session already ended", + testFunctionGeneratorRegular(/*shouldSucceed=*/false, /*endSession=*/true, "InvalidStateError"), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html new file mode 100644 index 0000000000..e8a83a62a6 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES, +}; + +// Creates a test method that leverages hit test API for transient input. +// |shouldSucceed| - true if the hit test request is expected to succeed, false otherwise +// |endSession| - true if the test case should call session.end() prior to requesting hit test +// |expectedError| - expected error name that should be returned in case shouldSucceed is false +const testFunctionGeneratorTransient = function(shouldSucceed, endSession, expectedError) { + const testFunction = function(session, fakeDeviceController, t) { + const hitTestOptionsInit = { + profile: "generic-touchscreen", + offsetRay: new XRRay(), + }; + + if(endSession) { + session.end(); + } + + return session.requestHitTestSourceForTransientInput(hitTestOptionsInit) + .then((hitTestSource) => { + t.step(() => { + assert_true(shouldSucceed, + "`requestHitTestSourceForTransientInput` succeeded when it was expected to fail"); + }); + }).catch((error) => { + t.step(() => { + assert_false(shouldSucceed, + "`requestHitTestSourceForTransientInput` failed when it was expected to succeed, error: " + error); + assert_equals(error.name, expectedError, + "`requestHitTestSourceForTransientInput` failed with unexpected error name"); + }); + }); + }; + + return testFunction; +}; + +xr_session_promise_test("Transient hit test subscription succeeds if the feature was requested", + testFunctionGeneratorTransient(/*shouldSucceed=*/true, /*endSession=*/false), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +xr_session_promise_test("Transient hit test subscription fails if the feature was not requested", + testFunctionGeneratorTransient(/*shouldSucceed=*/false, /*endSession=*/false, "NotSupportedError"), + fakeDeviceInitParams, + 'immersive-ar', {}); + +xr_session_promise_test("Transient test subscription fails if the feature was requested but the session already ended", + testFunctionGeneratorTransient(/*shouldSucceed=*/false, /*endSession=*/true, "InvalidStateError"), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html new file mode 100644 index 0000000000..9e0347963c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html @@ -0,0 +1,177 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, -0.25, 0], + orientation: [0, 0, 0, 1], +}; + +// Start the screen pointer at the same place as the viewer, so it's essentially +// coming straight forward from the middle of the screen. +const SCREEN_POINTER_TRANSFORM = VIEWER_ORIGIN_TRANSFORM; + +const screen_controller_init = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: SCREEN_POINTER_TRANSFORM, // aka mojo_from_pointer + profiles: ["generic-touchscreen",] +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka floor_from_mojo + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // see webxr_test_constants_fake_world.js for details +}; + +// Generates a test function given the parameters for the transient hit test. +// |ray| - ray that will be used to subscribe to hit test. +// |expectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +// |inputFromPointer| - input from pointer transform that will be used as the input source's +// inputFromPointer (aka pointer origin) in subsequent rAF. +// |nextFrameExpectedPoses| - array of expected pose objects. The poses should be expressed in local space. +// Null entries in the array mean that the given entry will not be validated. +let testFunctionGenerator = function(ray, expectedPoses, inputFromPointer, nextFrameExpectedPoses) { + const testFunction = function(session, fakeDeviceController, t) { + let debug = xr_debug.bind(this, 'testFunction'); + return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init); + + requestSkipAnimationFrame(session, (time, frame) => { + debug('rAF 1'); + t.step(() => { + assert_equals(session.inputSources.length, 1); + }); + + const hitTestOptionsInit = { + profile: "generic-touchscreen", + offsetRay: ray, + }; + + session.requestHitTestSourceForTransientInput(hitTestOptionsInit) + .then((hitTestSource) => { + t.step(() => { + assert_not_equals(hitTestSource, null); + }); + + // We got a hit test source, now get the results in subsequent rAFcb: + session.requestAnimationFrame((time, frame) => { + debug('rAF 2'); + const results = frame.getHitTestResultsForTransientInput(hitTestSource); + + t.step(() => { + assert_true(results != null, "Transient input hit tests should not be null"); + assert_equals(results.length, 1, "There should be exactly one group of transient hit test results!"); + assert_equals(results[0].results.length, expectedPoses.length); + for(const [index, expectedPose] of expectedPoses.entries()) { + const pose = results[0].results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "before-move-pose: "); + } + } + }); + + input_source_controller.setPointerOrigin(inputFromPointer, false); + + session.requestAnimationFrame((time, frame) => { + debug('rAF 3'); + const results = frame.getHitTestResultsForTransientInput(hitTestSource); + + t.step(() => { + assert_equals(results[0].results.length, nextFrameExpectedPoses.length); + for(const [index, expectedPose] of nextFrameExpectedPoses.entries()) { + const pose = results[0].results[index].getPose(localRefSpace); + assert_true(pose != null, "Each hit test result should have a pose in local space"); + if(expectedPose != null) { + assert_transform_approx_equals(pose.transform, expectedPose, FLOAT_EPSILON, "after-move-pose: "); + } + } + }); + + debug('resolving'); + resolve(); + }); + }); + }); + }); + })); + }; + + return testFunction; +}; + + +// Pose of the first expected hit test result - straight ahead of the input source, viewer-facing. +const pose_1 = { + position: {x: 0.0, y: 1.0, z: -2.5, w: 1.0}, + orientation: {x: 0.0, y: -0.707, z: -0.707, w: 0.0}, + // Hit test API will set Y axis to the surface normal at the intersection point, + // Z axis towards the ray origin and X axis to cross product of Y axis & Z axis. + // If the surface normal and Z axis would be parallel, the hit test API + // will attempt to use `up` vector ([0, 1, 0]) as the Z axis, and if it so happens that Z axis + // and the surface normal would still be parallel, it will use the `right` vector ([1, 0, 0]) as the Z axis. + // In this particular case, `up` vector will work so the resulting pose.orientation + // becomes a rotation around [0, 1, 1] vector by 180 degrees. +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - no move", + testFunctionGenerator(new XRRay(), [pose_1], SCREEN_POINTER_TRANSFORM, [pose_1]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const moved_pointer_transform_1 = { + position: [0, 1, 0], + orientation: [ 0.707, 0, 0, 0.707 ] // 90 degrees around X axis = facing up +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - no results", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_1, []), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +const pose_2 = { + position: {x: -1.443, y: 1.0, z: -2.5, w: 1.0}, + // Intersection point will be on the same height as the viewer, on the front + // wall. Distance from the front wall to viewer is 2.5m, and we are rotating + // to the left, so X coordinate of the intersection point will be negative + // & equal to -2.5 * tan(30 deg) ~= 1.443m. + orientation: {x: 0.5, y: 0.5, z: 0.5, w: 0.5 }, + // See comment for pose_1.orientation for details. + // In this case, the hit test pose will have Y axis facing towards world's + // positive Z axis ([0,0,1]), Z axis to the right ([1,0,0]) and X axis + // towards world's Y axis ([0,1,0]). + // This is equivalent to the rotation around [1, 1, 1] vector by 120 degrees. +}; + +const moved_pointer_transform_2 = { + position: [0, 1, 0], + orientation: [ 0, 0.2588, 0, 0.9659 ] // 30 degrees around Y axis = to the left, + // creating 30-60-90 triangle with the front wall +}; + +xr_session_promise_test("Ensures subscription to transient hit test works with an XRSpace from input source - after move - 1 result", + testFunctionGenerator(new XRRay(), [pose_1], moved_pointer_transform_2, [pose_2]), + fakeDeviceInitParams, + 'immersive-ar', { 'requiredFeatures': ['hit-test'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html new file mode 100644 index 0000000000..d1cb1a5a5c --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_constants_fake_world.js"></script> + +<script> + +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; + +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0.25, 0], + orientation: [0, 0, 0, 1], +}; + +const fakeDeviceInitParams = { + supportedModes: ["immersive-ar"], + views: VALID_VIEWS, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, // aka mojo_from_floor + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, // aka mojo_from_viewer + supportedFeatures: ALL_FEATURES, + world: createFakeWorld(5.0, 2.0, 5.0), // webxr_test_constants_fake_world.js has detailed description of the fake world +}; + +// Generates a test function given the parameters for the hit test. It will subscribe +// to a hit test using |refSpaceName| and |ray|, and attempt to obtain poses from returned hit +// test results using a space that is known to be unlocalizable, with the expectation of +// obtaining a null pose for each hit test result. +// |ray| - ray that will be used to subscribe to hit test. +// |refSpaceName| - XRReferenceSpaceType - either 'local', 'local-floor' or 'viewer'. +let testFunctionGenerator = function(ray, refSpaceName) { + const testFunction = function(session, fakeDeviceController, t) { + + const input_source_controller = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: IDENTITY_TRANSFORM, + profiles: [] + }); + + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + session.requestReferenceSpace('local-floor'), + ]).then(([localRefSpace, viewerRefSpace, localFloorRefSpace]) => { + + const refSpaceNameToSpace = { + 'local' : localRefSpace, + 'viewer' : viewerRefSpace, + 'local-floor' : localFloorRefSpace + }; + + const hitTestOptionsInit = { + space: refSpaceNameToSpace[refSpaceName], + offsetRay: ray, + }; + + return session.requestHitTestSource(hitTestOptionsInit).then( + (hitTestSource) => new Promise((resolve, reject) => { + + const requestAnimationFrameCallback = function(time, frame) { + + const hitTestResults = frame.getHitTestResults(hitTestSource); + + t.step(() => { + assert_true(session.inputSources.length > 0, "session.inputSources should not be empty!"); + assert_true(hitTestResults.length > 0, "Results should not be empty!"); + + const input_source = session.inputSources[0]; + + for(const [index, hitTestResult] of hitTestResults.entries()) { + const pose = hitTestResult.getPose(input_source.targetRaySpace); + assert_true(pose == null, "Pose should be null since input source is not localizable"); + } + }); + + resolve(); + }; + + t.step(() => { + assert_true(hitTestSource != null, "Hit test source should not be null"); + }); + + session.requestAnimationFrame(requestAnimationFrameCallback); + })); + }); + }; + + return testFunction; +}; + +// All test cases require local-floor and hit-test. +const sessionInit = { 'requiredFeatures': ['local-floor', 'hit-test'] }; + +xr_session_promise_test( + "Ensures hit test result returns null pose w/unlocalizable space - viewer space", + testFunctionGenerator(new XRRay(), 'viewer'), + fakeDeviceInitParams, 'immersive-ar', sessionInit); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/idlharness.https.html b/testing/web-platform/tests/webxr/hit-test/idlharness.https.html new file mode 100644 index 0000000000..f61959b9ed --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/idlharness.https.html @@ -0,0 +1,29 @@ +<!doctype html> +<title>WebXR hit-test IDL tests</title> +<link rel="help" href="https://immersive-web.github.io/hit-test/"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> + +<script> + 'use strict'; + + idl_test( + ['webxr-hit-test'], + ['webxr', 'geometry', 'dom'], + async idl_array => { + idl_array.add_objects({ + // TODO: XRHitTestSource + // TODO: XRTransientInputHitTestSource + // TODO: XRHitTestResult + // TODO: XRTransientInputHitTestResult + XRSession: ['xrSession'], + // TODO: XRFrame + XRRay: ['new XRRay()'], + }); + + self.xrSession = await navigator.xr.requestSession("inline"); + } + ); +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html b/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html new file mode 100644 index 0000000000..56b3931341 --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script> + +let constructor_test_name = "XRRay constructors work"; + +let constructor_tests = function() { + // Constructor tests for XRRay. + // Spec: https://immersive-web.github.io/webxr/#xrray-interface + + // + // Constructor 1 - from origin and direction + // + + { + // Check defaults - should be 0,0,0,1 for origin and 0,0,-1,0 for direction, + // identity matrix for the transform: + let xrRay1 = new XRRay(); + let xrRay2 = new XRRay({}); + let xrRay3 = new XRRay({}, {}); + + assert_point_approx_equals( + xrRay1.origin, {x : 0.0, y : 0.0, z : 0.0, w : 1.0}, + FLOAT_EPSILON, "origin-default:"); + assert_point_approx_equals( + xrRay1.direction, {x : 0.0, y : 0.0, z : -1.0, w : 0.0}, + FLOAT_EPSILON, "direction-default:"); + assert_matrix_approx_equals( + xrRay1.matrix, IDENTITY_MATRIX, + FLOAT_EPSILON, "matrix-default:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2-default:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3-default:"); + } + + { + // Check custom value for origin, default for direction: + let originDict = {x : 11.0, y : 12.0, z : 13.0, w : 1.0}; + let xrRay1 = new XRRay(originDict); + let xrRay2 = new XRRay(DOMPoint.fromPoint(originDict)); + let xrRay3 = new XRRay(DOMPointReadOnly.fromPoint(originDict)); + let matrix1 = [ 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 11, 12, 13, 1]; + + assert_point_approx_equals( + xrRay1.origin, originDict, + FLOAT_EPSILON, "origin-custom-direction-default:"); + assert_point_approx_equals( + xrRay1.direction, {x : 0.0, y : 0.0, z : -1.0, w : 0.0}, + FLOAT_EPSILON, "direction-custom-direction-default:"); + assert_matrix_approx_equals( + xrRay1.matrix, matrix1, + FLOAT_EPSILON, "matrix-custom-direction-default:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2-direction-default:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3-direction-default:"); + } + + { + // Check custom values - ray rotated from -Z onto +X, + // not placed at origin: + // - from DOMPoint + // - from DOMPointReadOnly + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + // column-major + let matrix1 = [ 0, 0, 1, 0, + 0, 1, 0, 0, + -1, 0, 0, 0, + 10, 10, 10, 1]; + + let xrRay1 = new XRRay( + originDict, + directionDict); + + let xrRay2 = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let xrRay3 = new XRRay( + DOMPointReadOnly.fromPoint(originDict), + DOMPointReadOnly.fromPoint(directionDict)); + + assert_point_approx_equals( + xrRay1.origin, originDict, + FLOAT_EPSILON, "origin-custom:"); + assert_point_approx_equals( + xrRay1.direction, directionNorm, + FLOAT_EPSILON, "direction-custom:"); + assert_matrix_approx_equals( + xrRay1.matrix, matrix1, + FLOAT_EPSILON, "matrix-custom:"); + + assert_ray_approx_equals(xrRay1, xrRay2, FLOAT_EPSILON, "ray1-ray2:"); + assert_ray_approx_equals(xrRay2, xrRay3, FLOAT_EPSILON, "ray2-ray3:"); + } + + { + // Check that we throw exception on direction too close to 0,0,0: + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint({x : 0.0, y : 0.0, z : 0.0, w : 0.0}) + ), "Constructor should throw for zero direction"); + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint({x : 1.0, y : 0.0, z : 0.0, w : 0.5}) + ), "Constructor should throw for nonzero direction w coordinate"); + + assert_throws_js(TypeError, () => new XRRay( + DOMPoint.fromPoint({x : 10.0, y : 10.0, z : 10.0, w : 0.5}), + DOMPoint.fromPoint(directionDict) + ), "Constructor should throw for non-1 origin w coordinate"); + } + + // + // Constructor 2 - from rigid transform. + // + + { + // Not placed at origin, ray rotated by 135 degrees around Y: + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionQuaternionDict = { x : 0, y : 0.9239, z : 0, w : 0.3827 }; + let directionNorm2 = { x : -0.707, y : 0.0, z : 0.707, w : 0.0 }; + let matrix2 = [-0.707, 0, -0.707, 0, + 0., 1, 0, 0, + 0.707, 0, -0.707, 0, + 10., 10, 10., 1]; + + let xrRay = new XRRay( + new XRRigidTransform( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionQuaternionDict))); + + assert_point_approx_equals( + xrRay.origin, originDict, + FLOAT_EPSILON, "origin-custom-rigid:"); + assert_point_approx_equals( + xrRay.direction, directionNorm2, + FLOAT_EPSILON, "direction-custom-rigid:"); + + assert_matrix_approx_equals( + xrRay.matrix, matrix2, + FLOAT_EPSILON, "matrix-custom-rigid:"); + } +}; + +test(constructor_tests, constructor_test_name); + +</script> diff --git a/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html b/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html new file mode 100644 index 0000000000..77f134a75f --- /dev/null +++ b/testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_test_constants.js"></script> +<script src="../resources/webxr_test_asserts.js"></script> +<script src="../resources/webxr_math_utils.js"></script> +<script> + +let matrix_tests_name = "XRRay matrix works"; + +let matrix_tests = function() { + // Matrix tests for XRRay. + // Spec: https://immersive-web.github.io/webxr/#xrray-interface + + const initialOrigin = {x : 0, y : 0, z : 0, w : 1}; + const initialDirection = {x : 0, y : 0, z : -1, w : 0}; + + // Test 1. Simple translation and rotation. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test1:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test1:"); + } + + // Test 2. Co-linear direction - rotation by 180 deg. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 0.0, y : 0.0, z : 1.0, w : 0.0}; + let directionNorm = {x : 0.0, y : 0.0, z : 1.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test2:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test2:"); + } + + // Test 3. No translation. + { + let originDict = {x : 0.0, y : 0.0, z : 0.0, w : 1.0}; + let directionDict = {x : 10.0, y : 0.0, z : 0.0, w : 0.0}; + let directionNorm = {x : 1.0, y : 0.0, z : 0.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test3:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test3:"); + } + + // Test 4. No rotation. + { + let originDict = {x : 10.0, y : 10.0, z : 10.0, w : 1.0}; + let directionDict = {x : 0.0, y : 0.0, z : -1.0, w : 0.0}; + let directionNorm = {x : 0.0, y : 0.0, z : -1.0, w : 0.0}; + let xrRay = new XRRay( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(directionDict)); + + let transformedOrigin = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialOrigin)); + let transformedDirection = normalize_perspective(transform_point_by_matrix(xrRay.matrix, initialDirection)); + + assert_point_approx_equals( + originDict, transformedOrigin, + FLOAT_EPSILON, "origin-test4:"); + assert_point_approx_equals( + directionNorm, transformedDirection, + FLOAT_EPSILON, "direction-test4:"); + } +}; + +test(matrix_tests, matrix_tests_name); + +</script> diff --git a/testing/web-platform/tests/webxr/idlharness.https.window.js b/testing/web-platform/tests/webxr/idlharness.https.window.js new file mode 100644 index 0000000000..dae201ebc9 --- /dev/null +++ b/testing/web-platform/tests/webxr/idlharness.https.window.js @@ -0,0 +1,55 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +// https://immersive-web.github.io/webxr/ + +idl_test( + ['webxr'], + ['permissions', 'webgl1', 'geometry', 'html', 'dom'], + async idl_array => { + idl_array.add_objects({ + Navigator: ['navigator'], + XR: ['navigator.xr'], + // TODO: XRSystem + XRSession: ['xrSession'], + XRRenderState: ['xrRenderState'], + // TODO: XRFrame + // TODO: XRSpace + XRReferenceSpace: ['xrReferenceSpace'], + // TODO: XRBoundedReferenceSpace + // TODO: XRView + // TODO: XRViewport + XRRigidTransform: ['new XRRigidTransform()'], + // TODO: XRPose + // TODO: XRViewerPose + // TODO: XRInputSource + XRInputSourceArray: ['xrInputSourceArray'], + XRWebGLLayer: ['xrWebGLLayer'], + WebGLRenderingContextBase: ['webGLRenderingContextBase'], + XRSessionEvent: ['xrSessionEvent'], + // TODO: XRInputSourceEvent + XRInputSourcesChangeEvent: ['xrInputSourcesChangeEvent'], + // TODO: XRReferenceSpaceEvent + // TODO: XRPermissionStatus + }); + + self.xrSession = await navigator.xr.requestSession('inline'); + self.xrRenderState = self.xrSession.renderState; + self.xrReferenceSpace = await self.xrSession.requestReferenceSpace('viewer'); + self.xrInputSourceArray = self.xrSession.inputSources; + self.xrSessionEvent = new XRSessionEvent('end', {session: self.xrSession}); + self.xrInputSourcesChangeEvent = new XRInputSourcesChangeEvent('inputsourceschange', { + session: self.xrSession, + added: [], + removed: [], + }); + + // XRWebGLRenderingContext is a typedef to either WebGLRenderingContext or WebGL2RenderingContext. + const canvas = document.createElement('canvas'); + self.webGLRenderingContextBase = canvas.getContext('webgl'); + self.xrWebGLLayer = new XRWebGLLayer(self.xrSession, self.webGLRenderingContextBase); + } +); diff --git a/testing/web-platform/tests/webxr/layers/META.yml b/testing/web-platform/tests/webxr/layers/META.yml new file mode 100644 index 0000000000..117c1adf02 --- /dev/null +++ b/testing/web-platform/tests/webxr/layers/META.yml @@ -0,0 +1 @@ +spec: https://immersive-web.github.io/layers/ diff --git a/testing/web-platform/tests/webxr/layers/xrSession_updateRenderState.https.html b/testing/web-platform/tests/webxr/layers/xrSession_updateRenderState.https.html new file mode 100644 index 0000000000..52f4b087bc --- /dev/null +++ b/testing/web-platform/tests/webxr/layers/xrSession_updateRenderState.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> + +<script> + + function testUpdateRenderState(xrSession, deviceController, t, { gl, glLayer }) { + return new Promise((resolve, reject) => { + const baseLayer = new XRWebGLLayer(xrSession, gl); + const singleLayer = [ + new XRWebGLLayer(xrSession, gl), + ]; + const multipleLayers = [ + new XRWebGLLayer(xrSession, gl), + new XRWebGLLayer(xrSession, gl) + ]; + const duplicateLayers = [ + baseLayer, + new XRWebGLLayer(xrSession, gl), + baseLayer + ]; + const layersFeatureEnabled = xrSession.enabledFeatures.includes('layers'); + + t.step(() => { + assert_throws_dom('NotSupportedError', () => xrSession.updateRenderState({ baseLayer, layers: singleLayer }), "XRSession should throw an error when updating render state with both a baseLayer and layers set."); + }); + + t.step(() => { + const updateRenderStateMultilayer = () => xrSession.updateRenderState({ layers: multipleLayers }); + if (layersFeatureEnabled) { + try { + updateRenderStateMultilayer(); + } catch (err) { + reject("XRSession should support render state with multiple layers if the layers feature is enabled."); + } + } else { + assert_throws_dom('NotSupportedError', updateRenderStateMultilayer, "XRSession should be able to updateRenderState with multiple layers only if the layers feature is enabled."); + } + }); + + if (layersFeatureEnabled) { + t.step(() => { + assert_throws_js(TypeError, () => xrSession.updateRenderState({ layers: duplicateLayers }), "XRSession should throw a TypeError when updating render state with duplicate layers."); + }); + } + + t.step(() => { + navigator.xr.requestSession('inline', {}).then((otherSession) => { + const otherSessionLayer = [ new XRWebGLLayer(otherSession, gl) ]; + assert_throws_js(TypeError, () => xrSession.updateRenderState({ layers: otherSessionLayer }), "XRSession should throw a TypeError when updating render state with a layer created for a different session."); + resolve(); + }); + }); + }); + } + + xr_session_promise_test("Ensure XRSession throws appropriate errors when updating render state without layers feature enabled", + testUpdateRenderState, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {}); + + xr_session_promise_test("Ensure XRSession throws appropriate errors when updating render state with layers feature enabled", + testUpdateRenderState, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', { requiredFeatures: ['layers'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/layers/xrWebGLBinding_constructor.https.html b/testing/web-platform/tests/webxr/layers/xrWebGLBinding_constructor.https.html new file mode 100644 index 0000000000..b3457cf320 --- /dev/null +++ b/testing/web-platform/tests/webxr/layers/xrWebGLBinding_constructor.https.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/webxr_util.js"></script> +<script src="../resources/webxr_test_constants.js"></script> + +<script> + +function testConstructor(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then(() => { + return navigator.xr.requestSession('inline') + .then((session) => { + try { + let webglLayerIncompatible = new XRWebGLBinding(session, gl); + assert_unreached("XRWebGLBinding should fail when created with an inline session."); + } catch (err) { + assert_equals(err.name, "InvalidStateError", "Should get InvalidStateError for creating with inline session."); + } + }); + }) + .then(() => { + return new Promise((resolve) => { + navigator.xr.test.simulateUserActivation(() => { + let xrSession = null; + navigator.xr.requestSession('immersive-vr') + .then((session) => { + xrSession = session; + t.step_func(() => { + try { + let webglLayerIncompatible = new XRWebGLBinding(xrSession, gl); + assert_unreached("XRWebGLBinding should fail when created with a context that is not XRCompatible.") + } catch (err) { + assert_equals(err.name, "InvalidStateError", "Should get InvalidStateError for non-XRCompatible context."); + } + }) + + return gl.makeXRCompatible(); + }).then(() => { + try { + let webglLayerGood = new XRWebGLBinding(xrSession, gl); + } catch (err) { + reject("XRWebGLBinding should not fail with valid arguments."); + } + + let lose_context_ext = gl.getExtension('WEBGL_lose_context'); + + gl.canvas.addEventListener('webglcontextlost', (ev) => { + ev.preventDefault(); + + try { + let webglLayerBadContext = new XRWebGLBinding(xrSession, gl); + reject("XRWebGLBinding should fail when created with a lost context."); + } catch (err) { + assert_equals(err.name, 'InvalidStateError', "Should get InvalidStateError for lost context."); + t.step_timeout(() => { lose_context_ext.restoreContext(); }, 100); + } + }); + + gl.canvas.addEventListener('webglcontextrestored', (ev) => { + resolve(xrSession.end().then(() => { + try { + let webglLayerBadSession = new XRWebGLBinding(xrSession, gl); + assert_unreached("XRWebGLBinding should fail when created with an ended session."); + } catch (err) { + assert_equals(err.name, 'InvalidStateError', "Should get InvalidStateError when passed an ended session."); + } + })); + }); + + lose_context_ext.loseContext(); + }); + }); + }); + }); +} +xr_promise_test("Ensure that XRWebGLBinding's constructor throws appropriate errors using webgl", + testConstructor, null, 'webgl'); + +xr_promise_test("Ensure that XRWebGLBinding's constructor throws appropriate errors using webgl2", + testConstructor, null, 'webgl2'); + +</script> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_oldSession.https.html b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_oldSession.https.html new file mode 100644 index 0000000000..7a896aa9ff --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_oldSession.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let testName = "getLightEstimate rejects if probe is from wrong session"; + let testFunction = (session, controller, t, sessionObjects) => new Promise((resolve) => { + let staleLightProbe = null; + let newSession = null; + + function onFrame(time, frame) { + t.step(() => { + // Attempting to get a lightEstimate with a probe created for a + // different session should throw an exception. + assert_throws_dom('InvalidStateError', () => frame.getLightEstimate(staleLightProbe)); + }); + + // Cleanup the new session we made and then resolve. + resolve(newSession.end()); + } + + // Request a default lightProbe + let probeInit = {reflectionFormat: session.preferredReflectionFormat }; + session.requestLightProbe(probeInit).then((probe) => { + staleLightProbe = probe; + return session.end(); + }).then(() => { + // Need to request a new session. + navigator.xr.test.simulateUserActivation( () => { + navigator.xr.requestSession('immersive-ar', {'requiredFeatures': ['light-estimation']}) + .then((session2) => { + + let glLayer = new XRWebGLLayer(session2, sessionObjects.gl); + glLayer.context = sessionObjects.gl; + // Session must have a baseLayer or frame requests will be ignored. + session2.updateRenderState({ + baseLayer: glLayer + }); + newSession = session2; + newSession.requestAnimationFrame(onFrame); + }); + }); + }); + }); + + xr_session_promise_test( + testName, + testFunction, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', + {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_staleFrame.https.html b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_staleFrame.https.html new file mode 100644 index 0000000000..499a30d561 --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_staleFrame.https.html @@ -0,0 +1,47 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let testName = "Cannot get XrLightEstimate from stale frame"; + let testFunction = (session, controller, t) => new Promise((resolve) => { + let lightProbe = null; + let staleFrame = null; + + function onFrame(time, frame) { + // Try to get the light estimate (even if it's null), it shouldn't throw. + let estimate = frame.getLightEstimate(lightProbe); + staleFrame = frame; + + t.step_timeout(afterFrame, 10); + } + + function afterFrame() { + t.step(() => { + // Attempting to call a method on the frame outside the callback that + // originally provided it should cause it to throw an exception. + assert_throws_dom('InvalidStateError', () => staleFrame.getLightEstimate(lightProbe)); + }); + + resolve(); + } + + // Request a default lightProbe + let probeInit = {reflectionFormat: session.preferredReflectionFormat}; + session.requestLightProbe(probeInit).then((probe) => { + lightProbe = probe; + session.requestAnimationFrame(onFrame); + }); + }); + + xr_session_promise_test( + testName, + testFunction, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_valid.https.html b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_valid.https.html new file mode 100644 index 0000000000..68c5d841fc --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_valid.https.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_asserts.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let testName = "Can get XRLightEstimates during frame"; + let fakeDeviceInitParams = IMMERSIVE_AR_DEVICE; + + let fakeEstimateCoefficients = [ + 0.01, 0.02, 0.03, + 0.04, 0.05, 0.06, + 0.07, 0.08, 0.09, + 0.10, 0.11, 0.12, + 0.13, 0.14, 0.15, + 0.16, 0.17, 0.18, + 0.19, 0.20, 0.21, + 0.22, 0.23, 0.24, + 0.25, 0.26, 0.27 + ]; + + let fakeDirectionInit = { x: 1.0, y: 0.0, z: 0.0, w: 0.0 }; + let fakeIntensityInit = { x: 0.0, y: 0.0, z: 1.0, w: 1.0 }; + + let testFunction = (session, controller, t) => new Promise((resolve) => { + let lightProbe = null; + function onFrameWithNoLightEstimation(time, frame) { + let estimate = frame.getLightEstimate(lightProbe); + t.step(() => { + assert_equals(estimate, null); + }); + + controller.setLightEstimate({ + sphericalHarmonicsCoefficients: fakeEstimateCoefficients + }); + + requestSkipAnimationFrame(session, onFrameWithCoefficients); + } + + function onFrameWithCoefficients(time, frame) { + let estimate = frame.getLightEstimate(lightProbe); + t.step(() => { + assert_not_equals(estimate, null); + assert_equals(estimate.sphericalHarmonicsCoefficients.length, 27); + assert_point_approx_equals(estimate.primaryLightDirection, { x: 0.0, y: 1.0, z: 0.0, w: 0.0 }); + assert_point_approx_equals(estimate.primaryLightIntensity, { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }); + }); + + controller.setLightEstimate({ + sphericalHarmonicsCoefficients: fakeEstimateCoefficients, + primaryLightDirection: fakeDirectionInit, + }); + + requestSkipAnimationFrame(session, onFrameWithDirection); + } + + function onFrameWithDirection(time, frame) { + let estimate = frame.getLightEstimate(lightProbe); + t.step(() => { + assert_not_equals(estimate, null); + assert_equals(estimate.sphericalHarmonicsCoefficients.length, 27); + assert_point_approx_equals(estimate.primaryLightDirection, fakeDirectionInit); + assert_point_approx_equals(estimate.primaryLightIntensity, { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }); + }); + + controller.setLightEstimate({ + sphericalHarmonicsCoefficients: fakeEstimateCoefficients, + primaryLightDirection: fakeDirectionInit, + primaryLightIntensity: fakeIntensityInit + }); + + requestSkipAnimationFrame(session, onFrameWithDirectionAndIntensity); + } + + function onFrameWithDirectionAndIntensity(time, frame) { + let estimate = frame.getLightEstimate(lightProbe); + t.step(() => { + assert_not_equals(estimate, null); + assert_equals(estimate.sphericalHarmonicsCoefficients.length, 27); + assert_point_approx_equals(estimate.primaryLightDirection, fakeDirectionInit); + assert_point_approx_equals(estimate.primaryLightIntensity, fakeIntensityInit); + }); + + resolve(); + } + + // Request a default lightProbe + session.requestLightProbe({reflectionFormat: session.preferredReflectionFormat }).then((probe) => { + lightProbe = probe; + session.requestAnimationFrame(onFrameWithNoLightEstimation); + }); + }); + + xr_session_promise_test( + testName, + testFunction, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_ended.https.html b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_ended.https.html new file mode 100644 index 0000000000..cc046499f9 --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_ended.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + xr_session_promise_test( + "getLightProbe rejects on an ended session", + (session, controller, t) => { + return session.end().then(() => { + return promise_rejects_dom(t, "InvalidStateError", session.requestLightProbe()) + }) + }, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', + {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_notEnabled.https.html b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_notEnabled.https.html new file mode 100644 index 0000000000..23fe1c6ec5 --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_notEnabled.https.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let fakeDeviceInitParams = IMMERSIVE_AR_DEVICE; + + xr_session_promise_test( + "getLightProbe rejects if not enabled on session", + (session, controller, t) => promise_rejects_dom(t, "NotSupportedError", session.requestLightProbe()), + IMMERSIVE_AR_DEVICE, + 'immersive-ar'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_valid.https.html b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_valid.https.html new file mode 100644 index 0000000000..074c7fd1dc --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_valid.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let testName = "Can create valid XRLightProbe objects"; + + function testFunction(session, controller, t) { + // Request a default lightProbe + let defaultProbe = session.requestLightProbe(); + let srgba8Probe = session.requestLightProbe({reflectionFormat: "srgba8"}); + let preferredProbe = session.requestLightProbe({reflectionFormat: session.preferredReflectionFormat }); + + return Promise.all([defaultProbe, srgba8Probe, preferredProbe]); + } + + xr_session_promise_test( + testName, + testFunction, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/light-estimation/xrWebGLBinding_getReflectionCubeMap.https.html b/testing/web-platform/tests/webxr/light-estimation/xrWebGLBinding_getReflectionCubeMap.https.html new file mode 100644 index 0000000000..a68a0f2b51 --- /dev/null +++ b/testing/web-platform/tests/webxr/light-estimation/xrWebGLBinding_getReflectionCubeMap.https.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="../resources/webxr_util.js"></script> + <script src="../resources/webxr_test_constants.js"></script> + + <script> + let testName = "Test that getReflectionCubeMap returns or throws appropriately without a reflection map."; + + let testFunction = (session, controller, t, sessionObjects) => new Promise((resolve) => { + let halfFloatExt = sessionObjects.gl.getExtension('OES_texture_half_float'); + // The preferredReflectionFormat used below is set to "rgba16f" by default. + // This means half float textures must be supported in order to run this test + if (!halfFloatExt) { + resolve(session.end()); + } else { + let debug = xr_debug.bind(this, 'testFunction'); + let lightProbe1 = null; + let binding1 = new XRWebGLBinding(session, sessionObjects.gl); + + // Request a default lightProbe + session.requestLightProbe({reflectionFormat: session.preferredReflectionFormat }).then((probe) => { + // Stash and end session. + lightProbe1 = probe; + + debug("Querying first pair"); + t.step(() => { + assert_equals( + binding1.getReflectionCubeMap(lightProbe1), + null, + "Active binding and light probe shouldn't throw when requesting cube map"); + }); + + return session.end(); + }).then(() => { + // Need to request a new session. + navigator.xr.test.simulateUserActivation( () => { + navigator.xr.requestSession('immersive-ar', { 'requiredFeatures': ['light-estimation'] }) + .then((newSession) => { + let newBinding = new XRWebGLBinding(newSession, sessionObjects.gl); + newSession.requestLightProbe({ reflectionFormat: newSession.preferredReflectionFormat }).then((newProbe) => { + t.step(() => { + debug("Querying second pair"); + assert_equals( + newBinding.getReflectionCubeMap(newProbe), + null, + "Newly created binding and light probe shouldn't throw"); + + debug("Querying old pair"); + assert_throws_dom( + "InvalidStateError", + () => binding1.getReflectionCubeMap(lightProbe1), + "Binding created with an ended session should throw InvalidStateError"); + debug("Querying mismatched pair"); + assert_throws_dom( + "InvalidStateError", + () => newBinding.getReflectionCubeMap(lightProbe1), + "Querying binding with a probe with a different backing session should throw InvalidStateError"); + }); + debug("losing context"); + + // Trigger a context loss and verify that we are unable to get the reflectionCubeMap. + let lose_context_ext = sessionObjects.gl.getExtension('WEBGL_lose_context'); + + sessionObjects.gl.canvas.addEventListener('webglcontextlost', (ev) => { + ev.preventDefault(); + + t.step(() => { + assert_throws_dom( + "InvalidStateError", + () => newBinding.getReflectionCubeMap(newProbe), + "Querying for reflection cube map on a binding with context loss should throw InvalidStateError"); + }); + + resolve(newSession.end()); + }); + + lose_context_ext.loseContext(); + }); // Request second light probe + }); // Request second session + }); // SimulateUserActivation + }); // .then on session end + } // halfFloatExt + }); // testFunction + + xr_session_promise_test( + testName, + testFunction, + IMMERSIVE_AR_DEVICE, + 'immersive-ar', + {'requiredFeatures': ['light-estimation']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/navigator_xr_sameObject.https.html b/testing/web-platform/tests/webxr/navigator_xr_sameObject.https.html new file mode 100644 index 0000000000..2c3ea541a9 --- /dev/null +++ b/testing/web-platform/tests/webxr/navigator_xr_sameObject.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Navigator.xr meets [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + let xr = navigator.xr; + + return new Promise((resolve) => { + // Make sure the navigator.xr object is the same on each frame. + session.requestAnimationFrame((time, xrFrame) => { + t.step(() => { + assert_equals(navigator.xr, xr, "navigator.xr returns the same object"); + }); + session.requestAnimationFrame((time, xrFrame) => { + t.step(() => { + assert_equals(navigator.xr, xr, + "naivgator.xr returns the same object"); + }); + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/render_state_update.https.html b/testing/web-platform/tests/webxr/render_state_update.https.html new file mode 100644 index 0000000000..3801a42947 --- /dev/null +++ b/testing/web-platform/tests/webxr/render_state_update.https.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testSessionEnded = function(session, fakeDeviceController, t) { + return new Promise((resolve, reject) => { + resolve(session.end().then(() => { + t.step(() => { + assert_throws_dom('InvalidStateError', () => session.updateRenderState({})); + }); + })); + }); +}; + + +let testBaseLayer = function(session, fakeDeviceController, t, sessionObjects) { + return new Promise((resolve, reject) => { + navigator.xr.test.simulateUserActivation(() => { + navigator.xr.requestSession('inline').then((tempSession) => { + t.step(() => { + assert_not_equals(session, tempSession); + assert_throws_dom('InvalidStateError', () => session.updateRenderState({ baseLayer : new XRWebGLLayer(tempSession, sessionObjects.gl), })); + }); + }); + resolve(); + }); + }); +}; + +let testFieldOfView = function(session, fakeDeviceController, t) { + return new Promise((resolve, reject) => { + t.step(() => { + assert_throws_dom('InvalidStateError', () => session.updateRenderState({ inlineVerticalFieldOfView : Math.PI, })); + }); + resolve(); + }); +}; + +let testNoParams = function(session, fakeDeviceController, t) { + return new Promise((resolve, reject) => { + try { + session.updateRenderState({}); + } catch (err) { + assert_unreached("updateRenderState should not fail (actually not do anything) with no params"); + } + resolve(); + }); +}; + +let testParams = function(session, fakeDeviceController, t, sessionObjects) { + return new Promise((resolve, reject) => { + let gl = sessionObjects.gl; + try { + gl.makeXRCompatible().then(() => { + t.step(() => { + let fov = Math.PI; + let near = 0.2; + let far = 0.8; + let layer = new XRWebGLLayer(session, gl); + session.updateRenderState({ inlineVerticalFieldOfView: fov, depthNear: near, depthFar: far, baseLayer: layer }); + // The update can only happen between frame boundaries, updateRenderState only queues changes. + assert_not_equals(session.renderState.inlineVerticalFieldOfView, fov); + assert_not_equals(session.renderState.depthNear, near); + assert_not_equals(session.renderState.depthFar, far); + assert_not_equals(session.renderState.baseLayer, layer); + }); + }); + } catch (err) { + assert_unreached("updateRenderState should not fail when all params are specified"); + } + resolve(); + }); +}; + +let testName = "updateRenderState handles appropriately ended sessions"; +xr_session_promise_test(testName, testSessionEnded, fakeDeviceInitParams, 'immersive-vr'); + +testName = "updateRenderState handles appropriately baseLayers created with different sessions"; +xr_session_promise_test(testName, testBaseLayer, fakeDeviceInitParams, 'immersive-vr'); + +testName = "updateRenderState handles appropriately immersive sessions with specified inlineVerticalFieldOfView"; +xr_session_promise_test(testName, testFieldOfView, fakeDeviceInitParams, 'immersive-vr'); + +testName = "updateRenderState handles appropriately XRRenderStateInit with no params"; +xr_session_promise_test(testName, testNoParams, fakeDeviceInitParams, 'immersive-vr'); + +testName = "updateRenderState handles appropriately XRRenderStateInit params"; +xr_session_promise_test(testName, testParams, fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/render_state_vertical_fov_immersive.https.html b/testing/web-platform/tests/webxr/render_state_vertical_fov_immersive.https.html new file mode 100644 index 0000000000..c09cd7c174 --- /dev/null +++ b/testing/web-platform/tests/webxr/render_state_vertical_fov_immersive.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "inlineVerticalFieldOfView is set appropriately on immersively sessions"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve, reject) => { + // inlineVerticalFieldOfView should be null for immersive sessions; + t.step(() => { + assert_equals(session.renderState.inlineVerticalFieldOfView, null); + }); + + // Trying to set it should throw an exception + try { + session.updateRenderState({ + inlineVerticalFieldOfView: 1.0 + }); + + t.step(() => { + assert_unreached("Should not be able to set inlineVerticalFieldOfView on immersive sessions"); + }); + } catch(err) { + t.step(() => { + assert_equals(err.name, "InvalidStateError"); + }); + } + + resolve(); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/render_state_vertical_fov_inline.https.html b/testing/web-platform/tests/webxr/render_state_vertical_fov_inline.https.html new file mode 100644 index 0000000000..3b33fb15c3 --- /dev/null +++ b/testing/web-platform/tests/webxr/render_state_vertical_fov_inline.https.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "inlineVerticalFieldOfView is set appropriately on inline sessions"; + +let fakeDeviceInitParams = VALID_NON_IMMERSIVE_DEVICE; + +// These are the min, max, and default from the WebXR Spec +let minFOV = 0.0; +let maxFOV = Math.PI; +let defaultFOV = Math.PI/2; + +function assertNotEquals(n1, n2, message) { + assert_greater_than(Math.abs(n1-n2), FLOAT_EPSILON, message); +} + +let testFunction = function(session, fakeDeviceController, t) { + // Helper method because the renderState does not (per the spec) get updated + // until the next rAF after it was updated, so this method returns a promise + // which will resolve when the updated state should be applied. + function updateAndApplyInlineFOV(fov) { + session.updateRenderState({ + inlineVerticalFieldOfView: fov + }); + + return new Promise((resolve, reject) => { + session.requestAnimationFrame(() => { resolve(); }); + }); + } + + // Helper method to keep the line length reasonable with a long attribute name + // and ensure that the nullable value actually has a value. + function getFOV() { + let fov = session.renderState.inlineVerticalFieldOfView; + t.step(() => { + assert_not_equals(fov, null); + }); + + return fov; + } + + return new Promise((resolve, reject) => { + // Begin by validating that the default is set as expected/specced. + t.step(() => { + assert_approx_equals(getFOV(), defaultFOV, FLOAT_EPSILON, "default"); + }); + + // Set something below min, and assert that it is not set below the min, + // and significantly different from the default. + updateAndApplyInlineFOV(-10).then(() => { + + t.step(() => { + let currentFOV = getFOV(); + assert_greater_than(currentFOV, minFOV, "FOV must be set to something greater than min"); + assert_less_than(currentFOV, maxFOV, "FOV must be set to something less than max"); + assertNotEquals(currentFOV, defaultFOV, "FOV should no longer be set to the default"); + }); + + // Set something above the max and assert that it is set to the max. + updateAndApplyInlineFOV(10).then(()=> { + t.step(()=> { + let currentFOV = getFOV(); + assert_greater_than(currentFOV, minFOV, "FOV must be set to something greater than min"); + assert_less_than(currentFOV, maxFOV, "FOV must be set to something less than max"); + assertNotEquals(currentFOV, defaultFOV, "FOV should not be set to the default"); + }); + + // Set to something reasonable and assert that the value gets set. + let normalFOV = 1.5; + updateAndApplyInlineFOV(normalFOV).then(() => { + t.step(() => { + assert_approx_equals(getFOV(), normalFOV, FLOAT_EPSILON, "FOV within min and max should get set"); + }); + + resolve(); + }); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/resources/webxr_check.html b/testing/web-platform/tests/webxr/resources/webxr_check.html new file mode 100644 index 0000000000..2d8e5b387d --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_check.html @@ -0,0 +1,17 @@ +<script src=webxr_util.js></script> +<script> +'use strict'; +let definedObjects = []; +let undefinedObjects = []; + +forEachWebxrObject((obj, name) => { + if(obj == undefined) { + undefinedObjects.push(name); + } else { + definedObjects.push(name); + } +}); + +window.parent.postMessage({ undefinedObjects, definedObjects}, '*'); + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/webxr/resources/webxr_math_utils.js b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js new file mode 100644 index 0000000000..54a61c1854 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_math_utils.js @@ -0,0 +1,67 @@ +// |matrix| - Float32Array, |input| - point-like dict (must have x, y, z, w) +let transform_point_by_matrix = function(matrix, input) { + return { + x : matrix[0] * input.x + matrix[4] * input.y + matrix[8] * input.z + matrix[12] * input.w, + y : matrix[1] * input.x + matrix[5] * input.y + matrix[9] * input.z + matrix[13] * input.w, + z : matrix[2] * input.x + matrix[6] * input.y + matrix[10] * input.z + matrix[14] * input.w, + w : matrix[3] * input.x + matrix[7] * input.y + matrix[11] * input.z + matrix[15] * input.w, + }; +} + +// Creates a unit-length quaternion. +// |input| - point-like dict (must have x, y, z, w) +let normalize_quaternion = function(input) { + const length_squared = input.x * input.x + input.y * input.y + input.z * input.z + input.w * input.w; + const length = Math.sqrt(length_squared); + + return {x : input.x / length, y : input.y / length, z : input.z / length, w : input.w / length}; +} + +// Returns negated quaternion. +// |input| - point-like dict (must have x, y, z, w) +let flip_quaternion = function(input) { + return {x : -input.x, y : -input.y, z : -input.z, w : -input.w}; +} + +// |input| - point-like dict (must have x, y, z, w) +let conjugate_quaternion = function(input) { + return {x : -input.x, y : -input.y, z : -input.z, w : input.w}; +} + +let multiply_quaternions = function(q1, q2) { + return { + w : q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z, + x : q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y, + y : q1.w * q2.y - q1.x * q2.z + q1.y * q2.w + q1.z * q2.x, + z : q1.w * q2.z + q1.x * q2.y - q1.y * q2.x + q1.z * q2.w, + } +} + +// |point| - point-like dict (must have x, y, z, w) +let normalize_perspective = function(point) { + if(point.w == 0 || point.w == 1) return point; + + return { + x : point.x / point.w, + y : point.y / point.w, + z : point.z / point.w, + w : 1 + }; +} + +// |quaternion| - point-like dict (must have x, y, z, w), +// |input| - point-like dict (must have x, y, z, w) +let transform_point_by_quaternion = function(quaternion, input) { + const q_normalized = normalize_quaternion(quaternion); + const q_conj = conjugate_quaternion(q_normalized); + const p_in = normalize_perspective(input); + + // construct a quaternion out of the point (take xyz & zero the real part). + const p = {x : p_in.x, y : p_in.y, z : p_in.z, w : 0}; + + // transform the input point + const p_mul = multiply_quaternions( q_normalized, multiply_quaternions(p, q_conj) ); + + // add back the w component of the input + return { x : p_mul.x, y : p_mul.y, z : p_mul.z, w : p_in.w }; +} diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js new file mode 100644 index 0000000000..a82f7aaf90 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_asserts.js @@ -0,0 +1,185 @@ +// Utility assert functions. +// Relies on resources/testharness.js to be included before this file. +// Relies on webxr_test_constants.js to be included before this file. +// Relies on webxr_math_utils.js to be included before this file. + + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// Returns the name of mismatching component between p1 and p2. +const get_mismatched_component = function(p1, p2, epsilon = FLOAT_EPSILON) { + for (const v of ['x', 'y', 'z', 'w']) { + if (Math.abs(p1[v] - p2[v]) > epsilon) { + return v; + } + } + + return null; +} + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_point_approx_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") { + if (p1 == null && p2 == null) { + return; + } + + assert_not_equals(p1, null, prefix + "p1 must be non-null"); + assert_not_equals(p2, null, prefix + "p2 must be non-null"); + + const mismatched_component = get_mismatched_component(p1, p2, epsilon); + + if (mismatched_component !== null) { + let error_message = prefix + ' Point comparison failed.\n'; + error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`; + error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`; + error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`; + assert_approx_equals(p2[mismatched_component], p1[mismatched_component], epsilon, error_message); + } +}; + +const assert_orientation_approx_equals = function(q1, q2, epsilon = FLOAT_EPSILON, prefix = "") { + if (q1 == null && q2 == null) { + return; + } + + assert_not_equals(q1, null, prefix + "q1 must be non-null"); + assert_not_equals(q2, null, prefix + "q2 must be non-null"); + + const q2_flipped = flip_quaternion(q2); + + const mismatched_component = get_mismatched_component(q1, q2, epsilon); + const mismatched_component_flipped = get_mismatched_component(q1, q2_flipped, epsilon); + + if (mismatched_component !== null && mismatched_component_flipped !== null) { + // q1 doesn't match neither q2 nor -q2, so it definitely does not represent the same orientations, + // log an assert failure. + let error_message = prefix + ' Orientation comparison failed.\n'; + error_message += ` p1: {x: ${q1.x}, y: ${q1.y}, z: ${q1.z}, w: ${q1.w}}\n`; + error_message += ` p2: {x: ${q2.x}, y: ${q2.y}, z: ${q2.z}, w: ${q2.w}}\n`; + error_message += ` Difference in component ${mismatched_component} exceeded the given epsilon.\n`; + assert_approx_equals(q2[mismatched_component], q1[mismatched_component], epsilon, error_message); + } +} + +// |p1|, |p2| - objects with x, y, z, w components that are floating point numbers. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_point_significantly_not_equals = function(p1, p2, epsilon = FLOAT_EPSILON, prefix = "") { + + assert_not_equals(p1, null, prefix + "p1 must be non-null"); + assert_not_equals(p2, null, prefix + "p2 must be non-null"); + + let mismatched_component = get_mismatched_component(p1, p2, epsilon); + + if (mismatched_component === null) { + let error_message = prefix + ' Point comparison failed.\n'; + error_message += ` p1: {x: ${p1.x}, y: ${p1.y}, z: ${p1.z}, w: ${p1.w}}\n`; + error_message += ` p2: {x: ${p2.x}, y: ${p2.y}, z: ${p2.z}, w: ${p2.w}}\n`; + error_message += ` Difference in components did not exceeded the given epsilon.\n`; + assert_unreached(error_message); + } +}; + +// |t1|, |t2| - objects containing position and orientation. +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_transform_approx_equals = function(t1, t2, epsilon = FLOAT_EPSILON, prefix = "") { + if (t1 == null && t2 == null) { + return; + } + + assert_not_equals(t1, null, prefix + "t1 must be non-null"); + assert_not_equals(t2, null, prefix + "t2 must be non-null"); + + assert_point_approx_equals(t1.position, t2.position, epsilon, prefix + "positions must be equal"); + assert_orientation_approx_equals(t1.orientation, t2.orientation, epsilon, prefix + "orientations must be equal"); +}; + +// |m1|, |m2| - arrays of floating point numbers +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_matrix_approx_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") { + if (m1 == null && m2 == null) { + return; + } + + assert_not_equals(m1, null, prefix + "m1 must be non-null"); + assert_not_equals(m2, null, prefix + "m2 must be non-null"); + + assert_equals(m1.length, 16, prefix + "m1 must have length of 16"); + assert_equals(m2.length, 16, prefix + "m2 must have length of 16"); + + let mismatched_element = -1; + for (let i = 0; i < 16; ++i) { + if (Math.abs(m1[i] - m2[i]) > epsilon) { + mismatched_element = i; + break; + } + } + + if (mismatched_element > -1) { + let error_message = prefix + 'Matrix comparison failed.\n'; + error_message += ' Difference in element ' + mismatched_element + + ' exceeded the given epsilon.\n'; + + error_message += ' Matrix 1: [' + m1.join(',') + ']\n'; + error_message += ' Matrix 2: [' + m2.join(',') + ']\n'; + + assert_approx_equals( + m1[mismatched_element], m2[mismatched_element], epsilon, + error_message); + } +}; + +// |m1|, |m2| - arrays of floating point numbers +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_matrix_significantly_not_equals = function(m1, m2, epsilon = FLOAT_EPSILON, prefix = "") { + if (m1 == null && m2 == null) { + return; + } + + assert_not_equals(m1, null, prefix + "m1 must be non-null"); + assert_not_equals(m2, null, prefix + "m2 must be non-null"); + + assert_equals(m1.length, 16, prefix + "m1 must have length of 16"); + assert_equals(m2.length, 16, prefix + "m2 must have length of 16"); + + let mismatch = false; + for (let i = 0; i < 16; ++i) { + if (Math.abs(m1[i] - m2[i]) > epsilon) { + mismatch = true; + break; + } + } + + if (!mismatch) { + let m1_str = '['; + let m2_str = '['; + for (let i = 0; i < 16; ++i) { + m1_str += m1[i] + (i < 15 ? ', ' : ''); + m2_str += m2[i] + (i < 15 ? ', ' : ''); + } + m1_str += ']'; + m2_str += ']'; + + let error_message = prefix + 'Matrix comparison failed.\n'; + error_message += + ' No element exceeded the given epsilon ' + epsilon + '.\n'; + + error_message += ' Matrix A: ' + m1_str + '\n'; + error_message += ' Matrix B: ' + m2_str + '\n'; + + assert_unreached(error_message); + } +}; + +// |r1|, |r2| - XRRay objects +// |epsilon| - float specifying precision +// |prefix| - string used as a prefix for logging +const assert_ray_approx_equals = function(r1, r2, epsilon = FLOAT_EPSILON, prefix = "") { + assert_point_approx_equals(r1.origin, r2.origin, epsilon, prefix + "origin:"); + assert_point_approx_equals(r1.direction, r2.direction, epsilon, prefix + "direction:"); + assert_matrix_approx_equals(r1.matrix, r2.matrix, epsilon, prefix + "matrix:"); +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js new file mode 100644 index 0000000000..d0d4fa2747 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js @@ -0,0 +1,205 @@ +// assert_equals can fail when comparing floats due to precision errors, so +// use assert_approx_equals with this constant instead +const FLOAT_EPSILON = 0.001; + +// Identity matrix +const IDENTITY_MATRIX = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1]; + +const IDENTITY_TRANSFORM = { + position: [0, 0, 0], + orientation: [0, 0, 0, 1], +}; + +// A valid pose matrix/transform for when we don't care about specific values +// Note that these two should be identical, just different representations +const VALID_POSE_MATRIX = [0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + 1, 1, 1, 1]; + +const VALID_POSE_TRANSFORM = { + position: [1, 1, 1], + orientation: [0.5, 0.5, 0.5, 0.5] +}; + +const VALID_PROJECTION_MATRIX = + [1, 0, 0, 0, 0, 1, 0, 0, 3, 2, -1, -1, 0, 0, -0.2, 0]; + +// This is a decomposed version of the above. +const VALID_FIELD_OF_VIEW = { + upDegrees: 71.565, + downDegrees: -45, + leftDegrees:-63.4349, + rightDegrees: 75.9637 +}; + +// A valid input grip matrix for when we don't care about specific values +const VALID_GRIP = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 4, 3, 2, 1]; + +const VALID_GRIP_TRANSFORM = { + position: [4, 3, 2], + orientation: [0, 0, 0, 1] +}; + +// A valid input pointer offset for when we don't care about specific values +const VALID_POINTER = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 1, 1]; + +const VALID_POINTER_TRANSFORM = { + position: [0, 0, 1], + orientation: [0, 0, 0, 1] +}; + +// A Valid Local to floor matrix/transform for when we don't care about specific +// values. Note that these should be identical, just different representations. +const VALID_FLOOR_ORIGIN_MATRIX = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 1.65, -1, 1]; + +const VALID_FLOOR_ORIGIN = { + position: [-1.0, -1.65, 1.0], + orientation: [0, 0, 0, 1] +}; + +const VALID_BOUNDS = [ + { x: 3.0, z: -2.0 }, + { x: 3.5, z: 0.0 }, + { x: 3.0, z: 2.0 }, + { x: -3.0, z: 2.0 }, + { x: -3.5, z: 0.0 }, + { x: -3.0, z: -2.0 } +]; + +const VALID_RESOLUTION = { + width: 200, + height: 200 +}; + +const LEFT_OFFSET = { + position: [-0.1, 0, 0], + orientation: [0, 0, 0, 1] +}; + +const RIGHT_OFFSET = { + position: [0.1, 0, 0], + orientation: [0, 0, 0, 1] +}; + +const FIRST_PERSON_OFFSET = { + position: [0, 0.1, 0], + orientation: [0, 0, 0, 1] +}; + +const VALID_VIEWS = [{ + eye:"left", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: LEFT_OFFSET, + resolution: VALID_RESOLUTION + }, { + eye:"right", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: RIGHT_OFFSET, + resolution: VALID_RESOLUTION + }, +]; + +const VALID_SECONDARY_VIEWS = [{ + eye: "none", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: FIRST_PERSON_OFFSET, + resolution: VALID_RESOLUTION, + isFirstPersonObserver: true + } +]; + +const NON_IMMERSIVE_VIEWS = [{ + eye: "none", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: IDENTITY_TRANSFORM, + resolution: VALID_RESOLUTION, + } +]; + +const ALL_FEATURES = [ + 'viewer', + 'local', + 'local-floor', + 'bounded-floor', + 'unbounded', + 'hit-test', + 'dom-overlay', + 'light-estimation', + 'anchors', + 'depth-sensing', + 'secondary-views', + 'camera-access', + 'layers' +]; + +const TRACKED_IMMERSIVE_DEVICE = { + supportsImmersive: true, + supportedModes: [ "inline", "immersive-vr"], + views: VALID_VIEWS, + secondaryViews: VALID_SECONDARY_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "world-space" +}; + +const IMMERSIVE_AR_DEVICE = { + supportsImmersive: true, + supportedModes: [ "inline", "immersive-ar"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "additive", + interactionMode: "screen-space" +}; + +const VALID_NON_IMMERSIVE_DEVICE = { + supportsImmersive: false, + supportedModes: ["inline"], + views: NON_IMMERSIVE_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES, + environmentBlendMode: "opaque", + interactionMode: "screen-space" +}; + +const VALID_CONTROLLER = { + handedness: "none", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +const RIGHT_CONTROLLER = { + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +const SCREEN_CONTROLLER = { + handedness: "none", + targetRayMode: "screen", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] +}; + +// From: https://immersive-web.github.io/webxr/#default-features +const DEFAULT_FEATURES = { + "inline": ["viewer"], + "immersive-vr": ["viewer", "local"], + "immersive-ar": ["viewer", "local"], +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js new file mode 100644 index 0000000000..36890d398d --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js @@ -0,0 +1,78 @@ +'use strict'; + +// This file introduces constants used to mock depth data for depth sensing API. + +const convertDepthBufferToArrayBuffer = function (data, desiredFormat) { + if(desiredFormat == "luminance-alpha") { + const result = new ArrayBuffer(data.length * 2); // each entry has 2 bytes + const view = new Uint16Array(result); + + for(let i = 0; i < data.length; ++i) { + view[i] = data[i]; + } + + return new Uint8Array(result); + } else if(desiredFormat == "float32") { + const result = new ArrayBuffer(data.length * 4); // each entry has 4 bytes + const view = new Float32Array(result); + + for(let i = 0; i < data.length; ++i) { + view[i] = data[i]; + } + + return new Uint8Array(result); + } else { + throw new Error("Unrecognized data format!"); + } +} + +// Let's assume that the depth values are in cm, Xcm = x * 1/100m +const RAW_VALUE_TO_METERS = 1/100; + +const createDepthSensingData = function() { + const depthSensingBufferHeight = 5; + const depthSensingBufferWidth = 7; + const depthSensingBuffer = [ + 1, 1, 1, 1, 1, 1, 1, // first row + 1, 2, 3, 4, 5, 6, 7, + 1, 4, 9, 16, 25, 36, 49, + 1, 8, 27, 64, 125, 216, 343, + 1, 16, 81, 256, 625, 1296, 2401, + ]; // depthSensingBuffer value at column c, row r is Math.pow(c+1, r). + + // Let's assume that the origin of the depth buffer is in the bottom right + // corner, with X's growing to the left and Y's growing upwards. + // This corresponds to the origin at 2401 in the above matrix, with X axis + // growing from 2401 towards 1296, and Y axis growing from 2401 towards 343. + // This corresponds to a rotation around Z axis by 180 degrees, with origin at [1,1]. + const depthSensingBufferFromViewerTransform = { + position: [1, 1, 0], + orientation: [0, 0, 1, 0], + }; + + return { + depthData: convertDepthBufferToArrayBuffer(depthSensingBuffer, "luminance-alpha"), + width: depthSensingBufferWidth, + height: depthSensingBufferHeight, + normDepthBufferFromNormView: depthSensingBufferFromViewerTransform, + rawValueToMeters: RAW_VALUE_TO_METERS, + }; +}; + +const DEPTH_SENSING_DATA = createDepthSensingData(); + +// Returns expected depth value at |column|, |row| coordinates, expressed +// in depth buffer's coordinate system. +const getExpectedValueAt = function(column, row) { + return Math.pow(column+1, row) * RAW_VALUE_TO_METERS; +}; + +const VALID_DEPTH_CONFIG_CPU_USAGE = { + usagePreference: ['cpu-optimized'], + dataFormatPreference: ['luminance-alpha', 'float32'], +}; + +const VALID_DEPTH_CONFIG_GPU_USAGE = { + usagePreference: ['gpu-optimized'], + dataFormatPreference: ['luminance-alpha', 'float32'], +}; diff --git a/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js new file mode 100644 index 0000000000..7e428e2155 --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js @@ -0,0 +1,79 @@ +'use strict'; + +// This file introduces constants used to mock fake world for the purposes of hit test. + +// Generates FakeXRWorldInit dictionary with given dimensions. +// The generated fake world will have floor and front wall treated as planes, +// side walls treated as meshes, and ceiling treated as points. +// width - X axis, in meters +// height - Y axis, in meters +// length - Z axis, in meters +function createFakeWorld( + width, height, length, + front_wall_and_floor_type = "plane", + side_walls_type = "mesh", + ceiling_type = "point") { + // Vertices: + const BOTTOM_LEFT_FRONT = { x: -width/2, y: 0, z: -length/2, w: 1}; + const BOTTOM_RIGHT_FRONT = { x: width/2, y: 0, z: -length/2, w: 1}; + + const TOP_LEFT_FRONT = { x: -width/2, y: height, z: -length/2, w: 1}; + const TOP_RIGHT_FRONT = { x: width/2, y: height, z: -length/2, w: 1}; + + const BOTTOM_LEFT_BACK = { x: -width/2, y: 0, z: length/2, w: 1}; + const BOTTOM_RIGHT_BACK = { x: width/2, y: 0, z: length/2, w: 1}; + + const TOP_LEFT_BACK = { x: -width/2, y: height, z: length/2, w: 1}; + const TOP_RIGHT_BACK = { x: width/2, y: height, z: length/2, w: 1}; + + // Faces: + const FRONT_WALL_AND_FLOOR_FACES = [ + // Front wall: + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT] }, + { vertices: [BOTTOM_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_LEFT_FRONT] }, + // Floor: + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK] }, + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, BOTTOM_RIGHT_BACK] }, + ]; + + const CEILING_FACES = [ + // Ceiling: + { vertices: [TOP_LEFT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] }, + { vertices: [TOP_LEFT_FRONT, TOP_LEFT_BACK, TOP_RIGHT_BACK] }, + ]; + + const SIDE_WALLS_FACES = [ + // Left: + { vertices: [BOTTOM_LEFT_FRONT, TOP_LEFT_FRONT, TOP_LEFT_BACK] }, + { vertices: [BOTTOM_LEFT_FRONT, BOTTOM_LEFT_BACK, TOP_LEFT_BACK] }, + // Right: + { vertices: [BOTTOM_RIGHT_FRONT, TOP_RIGHT_FRONT, TOP_RIGHT_BACK] }, + { vertices: [BOTTOM_RIGHT_FRONT, BOTTOM_RIGHT_BACK, TOP_RIGHT_BACK] }, + ]; + + // Regions: + const FRONT_WALL_AND_FLOOR_REGION = { + type: front_wall_and_floor_type, + faces: FRONT_WALL_AND_FLOOR_FACES, + }; + + const SIDE_WALLS_REGION = { + type: side_walls_type, + faces: SIDE_WALLS_FACES, + }; + + const CEILING_REGION = { + type: ceiling_type, + faces: CEILING_FACES, + }; + + return { + hitTestRegions : [ + FRONT_WALL_AND_FLOOR_REGION, + SIDE_WALLS_REGION, + CEILING_REGION, + ] + }; +} + +const VALID_FAKE_WORLD = createFakeWorld(5.0, 2.0, 5.0); diff --git a/testing/web-platform/tests/webxr/resources/webxr_util.js b/testing/web-platform/tests/webxr/resources/webxr_util.js new file mode 100644 index 0000000000..625f76450e --- /dev/null +++ b/testing/web-platform/tests/webxr/resources/webxr_util.js @@ -0,0 +1,241 @@ +'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(); +} diff --git a/testing/web-platform/tests/webxr/webGLCanvasContext_create_xrcompatible.https.html b/testing/web-platform/tests/webxr/webGLCanvasContext_create_xrcompatible.https.html new file mode 100644 index 0000000000..3cdbce99cc --- /dev/null +++ b/testing/web-platform/tests/webxr/webGLCanvasContext_create_xrcompatible.https.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + + function testNoDevice(t, gl) { + assert_false(gl.getContextAttributes().xrCompatible); + return promise_rejects_dom(t, "InvalidStateError", gl.makeXRCompatible()); + } + + xr_promise_test("Creating a webgl context with no device", + testNoDevice, null, 'webgl'); + + xr_promise_test("Creating a webgl2 context with no device", + testNoDevice, null, 'webgl2'); + + function testOffscreenCanvas(canvas, glContextType) { + let gl = canvas.getContext('webgl'); + + return gl.makeXRCompatible().then(() => { + assert_true(gl.getContextAttributes().xrCompatible); + }); + } + + function testXrCompatible(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => { + return gl.makeXRCompatible(); + }).then( () => { + assert_true(gl.getContextAttributes().xrCompatible); + + return testOffscreenCanvas(document.createElement('canvas'), 'webgl'); + }).then( () => { + return testOffscreenCanvas(document.createElement('canvas'), 'webgl2'); + }).then( () => { + return testOffscreenCanvas(new OffscreenCanvas(1, 1), 'webgl'); + }).then( () => { + return testOffscreenCanvas(new OffscreenCanvas(1, 1), 'webgl2'); + }); + } + + xr_promise_test("An XR-compatible webgl context can be created", + testXrCompatible, null, 'webgl'); + + xr_promise_test("An XR-compatible webgl2 context can be created", + testXrCompatible, null, 'webgl2'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_contextlost.https.html b/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_contextlost.https.html new file mode 100644 index 0000000000..354c614792 --- /dev/null +++ b/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_contextlost.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + + function testContextLost(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => { + return gl.makeXRCompatible(); + }).then( () => { + gl.getExtension('WEBGL_lose_context').loseContext(); + return promise_rejects_dom(t, 'InvalidStateError', gl.makeXRCompatible()); + }); + } + + xr_promise_test( + "A lost webgl context should not be able to set xr compatibility", + testContextLost, null, 'webgl'); + + xr_promise_test( + "A lost webgl2 context should not be able to set xr compatibility", + testContextLost, null, 'webgl2'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_reentrant.https.html b/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_reentrant.https.html new file mode 100644 index 0000000000..f54a878d45 --- /dev/null +++ b/testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_reentrant.https.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_util.js"></script> +<script> + +function testNonReentrant(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then((controller) => { + assert_false(gl.getContextAttributes().xrCompatible); + return gl.makeXRCompatible(); + }).then(() => { + assert_true(gl.getContextAttributes().xrCompatible); + return gl.makeXRCompatible(); + }).then(() => { + assert_true(gl.getContextAttributes().xrCompatible); + }); +} + +xr_promise_test( + "Verify promise from a non-reentrant call to makeXRCompatible() is resolved for webgl", + testNonReentrant, null, 'webgl'); +xr_promise_test( + "Verify promise from a non-reentrant call to makeXRCompatible() is resolved for webgl2", + testNonReentrant, null, 'webgl2'); + +function testReentrant(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then((controller) => { + assert_false(gl.getContextAttributes().xrCompatible); + + return Promise.all([gl.makeXRCompatible(), + gl.makeXRCompatible(), + gl.makeXRCompatible()]); + }).then(() => { + assert_true(gl.getContextAttributes().xrCompatible); + }); +} + +xr_promise_test( + "Verify promises from reentrant calls to makeXRCompatible() are resolved for webgl", + testReentrant, null, 'webgl'); +xr_promise_test( + "Verify promises from reentrant calls to makeXRCompatible() are resolved for webgl2", + testReentrant, null, 'webgl2'); + +</script> diff --git a/testing/web-platform/tests/webxr/webxr-supported-by-feature-policy.html b/testing/web-platform/tests/webxr/webxr-supported-by-feature-policy.html new file mode 100644 index 0000000000..2843849a34 --- /dev/null +++ b/testing/web-platform/tests/webxr/webxr-supported-by-feature-policy.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<title>Test that xr is advertised in the feature list</title> +<link rel="help" href="https://immersive-web.github.io/webxr/#feature-policy"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(() => { + assert_in_array('xr-spatial-tracking', document.featurePolicy.features()); +}, 'document.featurePolicy.features should advertise xr-spatial-tracking.'); +</script> diff --git a/testing/web-platform/tests/webxr/webxr_availability.http.sub.html b/testing/web-platform/tests/webxr/webxr_availability.http.sub.html new file mode 100644 index 0000000000..515b2ad1a8 --- /dev/null +++ b/testing/web-platform/tests/webxr/webxr_availability.http.sub.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src=/webxr/resources/webxr_util.js></script> + <script> + 'use strict'; + + var same_origin_src = '/webxr/resources/'; + var cross_origin_https_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + + same_origin_src; + + test(t => { + forEachWebxrObject((obj, name) => { + assert_equals(obj, undefined, name + ' was defined in insecure context.'); + }); + }, 'Test webxr not available in insecure context'); + + async_test(t => { + let frame = document.createElement('iframe'); + frame.src = cross_origin_https_src + 'webxr_check.html'; + + window.addEventListener('message', t.step_func(function handler(evt) { + if (evt.source === frame.contentWindow) { + document.body.removeChild(frame); + window.removeEventListener('message', handler); + + assert_equals(evt.data.definedObjects.length, 0, + "Some objects were defined in insecure context: " + + evt.data.definedObjects.toString()); + t.done(); + } + })); + + document.body.appendChild(frame); + }, 'Test webxr not available in secure context in insecure context'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html b/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html new file mode 100644 index 0000000000..9146dd88f0 --- /dev/null +++ b/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<canvas /> + +<script> +xr_promise_test( +"Validate isSessionSupported behavior without xr-spatial-tracking policy", +(t) => { + // Inline should never reject. + return navigator.xr.isSessionSupported("inline").then((supported) => { + t.step(() => { + assert_true(supported, + "inline should always be supported, even without permissions policy"); + }); + + // It shouldn't matter that there's no device connected, the SecurityError + // should reject first. + return promise_rejects_dom(t, "SecurityError", + navigator.xr.isSessionSupported("immersive-vr"), + "Immersive isSessionSupported should reject"); + }); +}); + +xr_promise_test( +"Validate requestSession behavior without xr-spatial-tracking policy", +(t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then(() => { + return new Promise((resolve, reject) => { + navigator.xr.test.simulateUserActivation(() => { + + // Technically the first "requestSession" doesn't need either the device + // or the activation, but this makes the test code a little cleaner since + // the others do, as lacking user activation or a valid backing device + // should also cause the session to reject. In order to guarantee that + // we're seeing the rejection we want, we eliminate those as possibilities. + resolve(Promise.all([ + navigator.xr.requestSession("inline").then(session => session.end()), + + promise_rejects_dom(t, "NotSupportedError", + navigator.xr.requestSession("inline", { requiredFeatures: ["local"] }), + "Inline with features should reject without permissions policy"), + + promise_rejects_dom(t, "NotSupportedError", + navigator.xr.requestSession("immersive-vr"), + "Immersive-vr should reject without permissions policy") + ])); + }); + }); + }); +}); + +xr_promise_test( +"Validate devicechange event behavior without xr-spatial-tracking policy", +(t) => { + navigator.xr.addEventListener("devicechange", () => { + t.step(() => { assert_unreached("devicechange should not fire"); }); + }) + + // We need to yield a short time to ensure that any event registration has + // propagated, as this can take some time. + // Note that device connection is not guaranteed to fire per the spec, if it's + // the first connection, but disconnect definitely should. + t.step_timeout(() => { + navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then((testDeviceController) => { + return testDeviceController.disconnect(); + }); + }, 100); + + // Wait an even longer time before finishing the test, so that if the event + // were to fire, it would've by now. + return new Promise((resolve) => { + t.step_timeout(() => { resolve(); }, 2000); + }); + +}); + +xr_promise_test( +"Validate xr compatibility requests without xr-spatial-tracking policy", +(t) => { + let canvas = document.createElement('canvas'); + let gl = canvas.getContext('webgl', {xrCompatible: true}); + + t.step(() => { + assert_false(gl.getContextAttributes().xrCompatible, + "xrCompatibility shouldn't be set when requested without permissions policy"); + }); + + return promise_rejects_dom(t, "SecurityError", + gl.makeXRCompatible(), + "makeXRCompatible should reject without permissions policy"); +}); +</script> diff --git a/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html.headers b/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html.headers new file mode 100644 index 0000000000..56b8c11a5b --- /dev/null +++ b/testing/web-platform/tests/webxr/webxr_permissions_policy.https.html.headers @@ -0,0 +1 @@ +Permissions-Policy: xr-spatial-tracking=() diff --git a/testing/web-platform/tests/webxr/xrBoundedReferenceSpace_updates.https.html b/testing/web-platform/tests/webxr/xrBoundedReferenceSpace_updates.https.html new file mode 100644 index 0000000000..c5f9b7f325 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrBoundedReferenceSpace_updates.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> +let testName = + "'XRBoundedReferenceSpace updates properly when the changes are applied"; + +let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + floorOrigin: VALID_FLOOR_ORIGIN, + supportedFeatures: ALL_FEATURES +}; + +let testFunction = function(session, fakeDeviceController, t) { + + return new Promise((resolve, reject) => { + session.requestReferenceSpace('bounded-floor') + .then((referenceSpace) => { + t.step(() => { + // A bounded space may be created if no bounds have been set but the system has the capability to support bounded-floor + // A lack of bounds is indicated by an empty boundsGeometry + assert_equals(referenceSpace.boundsGeometry.length, 0); + }); + + function onFrame(time, xrFrame) { + // After the bounds have been explicitly set, they should be what we expect. + t.step(() => { + assert_equals(referenceSpace.boundsGeometry.length, VALID_BOUNDS.length); + for (i = 0; i < VALID_BOUNDS.length; ++i) { + let valid_point = VALID_BOUNDS[i]; + let bounds_point = referenceSpace.boundsGeometry[i]; + assert_equals(valid_point.x, bounds_point.x); + assert_equals(bounds_point.y, 0.0); + assert_equals(valid_point.z, bounds_point.z); + assert_equals(bounds_point.w, 1.0); + } + }); + + resolve(); + } + + // Now set the bounds explicitly and check again on the next frame. + fakeDeviceController.setBoundsGeometry(VALID_BOUNDS); + requestSkipAnimationFrame(session, onFrame); + }); + }); +}; + +xr_session_promise_test(testName, testFunction, fakeDeviceInitParams, 'immersive-vr', { 'requiredFeatures': ['bounded-floor'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/xrDevice_disconnect_ends.https.html b/testing/web-platform/tests/webxr/xrDevice_disconnect_ends.https.html new file mode 100644 index 0000000000..644a4d68e4 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_disconnect_ends.https.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + const testName = "Immersive session ends when device is disconnected"; + let watcherDone = new Event("watcherdone"); + const fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = function(session, testDeviceController, t) { + let sessionWatcher = new EventWatcher(t, session, ["end", "watcherdone"]); + let sessionPromise = sessionWatcher.wait_for(["end", "watcherdone"]); + + let xrWatcher = new EventWatcher(t, navigator.xr, ["devicechange"]); + let xrPromise = xrWatcher.wait_for(["devicechange"]); + + function onSessionEnd(event) { + t.step( () => { + assert_equals(event.session, session); + session.dispatchEvent(watcherDone); + }); + } + + session.addEventListener("end", onSessionEnd, false); + + // The javascript needs to yield so that the event registration processes. + t.step_timeout(() => { testDeviceController.disconnect(); }, 0); + + return Promise.all([sessionPromise, xrPromise]); + }; + + xr_session_promise_test(testName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive.https.html b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive.https.html new file mode 100644 index 0000000000..2f1129fe16 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "isSessionSupported resolves to true when immersive options supported", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => { + return navigator.xr.isSessionSupported('immersive-vr').then((supported) => { + t.step(() => { + assert_true(supported); + }); + }); + }); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive_unsupported.https.html b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive_unsupported.https.html new file mode 100644 index 0000000000..41ae338787 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive_unsupported.https.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "isSessionSupported resolves to false when options not supported", + (t) => { + return navigator.xr.test.simulateDeviceConnection(VALID_NON_IMMERSIVE_DEVICE) + .then( (controller) => { + return navigator.xr.isSessionSupported('immersive-vr').then((supported) => { + t.step(() => { + assert_false(supported); + }); + }); + }); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_inline.https.html b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_inline.https.html new file mode 100644 index 0000000000..bf8d14a8f5 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_isSessionSupported_inline.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "isSessionSupported resolves to true when inline options supported", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => { + // Inline sessions should be supported. + return navigator.xr.isSessionSupported('inline').then((supported) => { + t.step(() => { + assert_true(supported); + }); + }); + }); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html new file mode 100644 index 0000000000..6ff8821670 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_session_promise_test( + "Tests requestSession resolves when supported", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); + + xr_session_promise_test( + "Tests requestSession accepts XRSessionInit dictionary", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {}); + + xr_session_promise_test( + "Tests requestSession ignores unknown optionalFeatures", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {optionalFeatures: ['unicorns']}); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_no_gesture.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_no_gesture.https.html new file mode 100644 index 0000000000..b124a21230 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_no_gesture.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "Requesting immersive session outside of a user gesture rejects", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => promise_rejects_dom( + t, 'SecurityError', navigator.xr.requestSession('immersive-vr'))); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_unsupported.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_unsupported.https.html new file mode 100644 index 0000000000..9e3d2f0cd9 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_unsupported.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "Requesting an immersive session when unsupported rejects", + (t) => { + return navigator.xr.test.simulateDeviceConnection(VALID_NON_IMMERSIVE_DEVICE) + .then( (controller) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + resolve(promise_rejects_dom( + t, + "NotSupportedError", + navigator.xr.requestSession('immersive-vr') + )) + }); + })); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_no_mode.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_no_mode.https.html new file mode 100644 index 0000000000..9c1ebb7efd --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_no_mode.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "Requesting a session with no mode rejects", + (t) => { + return navigator.xr.test.simulateDeviceConnection(VALID_NON_IMMERSIVE_DEVICE) + .then( (controller) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + t.step_func(() => { + assert_throws_js(TypeError, () => navigator.xr.requestSession()) + }) + resolve() + }); + })); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html new file mode 100644 index 0000000000..5995059255 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_promise_test( + "Requesting non-immersive session outside of a user gesture succeeds", + (t) => { + return navigator.xr.test.simulateDeviceConnection(VALID_NON_IMMERSIVE_DEVICE) + .then( (controller) => navigator.xr.requestSession('inline')) + .then( (session) => { assert_not_equals(session, null); }); + }); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_optionalFeatures.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_optionalFeatures.https.html new file mode 100644 index 0000000000..f7737624a6 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_optionalFeatures.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + xr_session_promise_test( + "Tests requestSession accepts XRSessionInit dictionary", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {}); + + xr_session_promise_test( + "Tests requestSession accepts XRSessionInit dictionary with empty feature lists", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {requiredFeatures: [], optionalFeatures: []}); + + xr_session_promise_test( + "Tests requestSession ignores unknown strings in optionalFeatures", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {optionalFeatures: ['unicorns']}); + + xr_session_promise_test( + "Tests requestSession ignores unknown objects in optionalFeatures", + (session) => { + assert_not_equals(session, null); + }, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr', {optionalFeatures: [{ unicorns: "please" }]}); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrDevice_requestSession_requiredFeatures_unknown.https.html b/testing/web-platform/tests/webxr/xrDevice_requestSession_requiredFeatures_unknown.https.html new file mode 100644 index 0000000000..6da40af700 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_requiredFeatures_unknown.https.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <canvas></canvas> + <script> + xr_promise_test( + "Tests requestSession rejects for unknown requiredFeatures", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + resolve(promise_rejects_dom( + t, + "NotSupportedError", + navigator.xr.requestSession('immersive-vr', + {requiredFeatures: ['undefined-unicorns']}), + "unexpected requestSession success" + ).then(() => { + return promise_rejects_dom( + t, + "NotSupportedError", + navigator.xr.requestSession('immersive-vr', + {requiredFeatures: [{unicorns: "please"}]}), + "unexpected requestSession success with unknown object" + ); + })); + }); + })); + }); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrFrame_getPose.https.html b/testing/web-platform/tests/webxr/xrFrame_getPose.https.html new file mode 100644 index 0000000000..9cd7922f64 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrFrame_getPose.https.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let immersiveTestName = "XRFrame.getPose works for immersive sessions"; +let nonImmersiveTestName = "XRFrame.getPose works for non-immersive sessions"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('local') + ]).then((spaces) => new Promise((resolve) => { + function onFrame(time, xrFrame) { + const radians = Math.PI / 2.0; // 90 degrees + + // Both eye-level spaces start out with originOffset = identity matrix. + let space1 = spaces[0]; + let space2 = spaces[1]; + + let offset = new XRRigidTransform( + DOMPointReadOnly.fromPoint({ + x: 2, + y: 3, + z: 4, + w: 1, + })); + + let translatedSpace1 = space1.getOffsetReferenceSpace(offset); + let translated_from_base = xrFrame.getPose(translatedSpace1, space1); + + // Getting the transform of an offset space from the space it was based on + // should be the same as the initially applied offset. + t.step(() => { + assert_matrix_approx_equals(translated_from_base.transform.matrix, offset.matrix, FLOAT_EPSILON); + }); + + // Rotate 90 degrees about x axis, then move 1 meter along y axis. + space1 = space1.getOffsetReferenceSpace(new XRRigidTransform( + DOMPointReadOnly.fromPoint({ + x : 0, + y : 1, + z : 0, + w : 1 + }), + DOMPointReadOnly.fromPoint({ + x : Math.sin(radians / 2), + y : 0, + z : 0, + w : Math.cos(radians / 2) + }) + )); + + // Rotate 90 degrees about z axis, then move 1 meter along x axis. + space2 = space2.getOffsetReferenceSpace(new XRRigidTransform( + DOMPointReadOnly.fromPoint({ + x : 1, + y : 0, + z : 0, + w : 1 + }), + DOMPointReadOnly.fromPoint({ + x : 0, + y : 0, + z : Math.sin(radians / 2), + w : Math.cos(radians / 2) + }) + )); + + let space2_from_space1 = xrFrame.getPose(space1, space2); + const EXPECTED_POSE_MATRIX = [ + 0, -1, 0, 0, // 1st column + 0, 0, 1, 0, // 2nd column + -1, 0, 0, 0, // 3rd column + 1, 1, 0, 1 // 4th column + ]; + + t.step(() => { + assert_matrix_approx_equals(space2_from_space1.transform.matrix, EXPECTED_POSE_MATRIX, FLOAT_EPSILON); + }); + + // Finished test. + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); +}; + +xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); +xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline', { 'requiredFeatures': ['local'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose.https.html b/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose.https.html new file mode 100644 index 0000000000..05cbf78523 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose.https.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script src="resources/webxr_test_asserts.js"></script> + + <script> + + let testName = "XRFrame getViewerPose(refSpace) matches getPose(viewer, refSpace)."; + + // Used for viewer origin, the actual values should not matter for the test. + const poseTransform = { + position: [1, 2, 3], + orientation: [0.5, 0.5, 0.5, 0.5] // 120 degrees around [1, 1, 1] axis + }; + + // Use the same device as tracked immersive device, but modify the viewer origin. + const deviceInitParams = Object.assign({}, TRACKED_IMMERSIVE_DEVICE, {viewerOrigin: poseTransform}); + + // Used when creating a reference space that is offset from local space. + // Actual values should not matter for the test. + const offsetSpaceTransform = new XRRigidTransform( + { x: 1.00, y: -1.50, z: 10.00, w: 1.0 }, + { x: 0.27, y: 0.00, z: 0.27, w: 0.92}, // 45 degrees around [1, 0, 1] axis + ); + + const testFunction = function(session, fakeDeviceController, t) { + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + ]).then(([referenceSpace, viewerRefSpace]) => new Promise((resolve, reject) => { + const offsetRefSpace = referenceSpace.getOffsetReferenceSpace(offsetSpaceTransform); + + function onFrame(time, frame){ + t.step(() => { + const pose1 = frame.getViewerPose(referenceSpace); + const pose2 = frame.getPose(viewerRefSpace, referenceSpace); + assert_not_equals(pose1, null); + assert_not_equals(pose2, null); + assert_matrix_approx_equals(pose1.transform.matrix, pose2.transform.matrix); + + const pose3 = frame.getViewerPose(offsetRefSpace); + const pose4 = frame.getPose(viewerRefSpace, offsetRefSpace); + assert_not_equals(pose3, null); + assert_not_equals(pose4, null); + assert_matrix_approx_equals(pose3.transform.matrix, pose4.transform.matrix); + }); + + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); + }; + + xr_session_promise_test(testName, testFunction, + deviceInitParams, 'immersive-vr'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose_identities.https.html b/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose_identities.https.html new file mode 100644 index 0000000000..ba2269147e --- /dev/null +++ b/testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose_identities.https.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script src="resources/webxr_test_asserts.js"></script> + + <script> + + let testName = "XRFrame getViewerPose(viewerSpace) & getPose(space, space) return identity even during tracking loss"; + + // Use the same device as tracked immersive device, but modify the viewer origin. + const deviceInitParams = Object.assign({}, TRACKED_IMMERSIVE_DEVICE, {viewerOrigin: null}); + + // Used when creating a reference space that is offset from local space. + // Actual values should not matter for the test. + const offsetSpaceTransform = new XRRigidTransform( + { x: 1.00, y: -1.50, z: 10.00, w: 1.0 }, + { x: 0.27, y: 0.00, z: 0.27, w: 0.92}, // 45 degrees around [1, 0, 1] axis + ); + + const expectMatrix = function(pose, expectedMatrix, poseName) { + assert_not_equals(pose, null, + poseName + " should not be null!"); + assert_matrix_approx_equals(pose.transform.matrix, expectedMatrix, + poseName + "'s matrix should match expectations!"); + assert_false(pose.emulatedPosition, + poseName + ".emulatedPosition should be false!"); + } + + const expectIdentity = function(pose, poseName) { + assert_not_equals(pose, null, + poseName + " should not be null!"); + assert_matrix_approx_equals(pose.transform.matrix, IDENTITY_MATRIX, + poseName + "'s matrix should equal identity!"); + assert_false(pose.emulatedPosition, + poseName + ".emulatedPosition should be false!"); + } + + const testFunction = function(session, fakeDeviceController, t) { + return Promise.all([ + session.requestReferenceSpace('local'), + session.requestReferenceSpace('viewer'), + session.requestReferenceSpace('local-floor'), + session.requestReferenceSpace('bounded-floor'), + session.requestReferenceSpace('unbounded'), + session.requestReferenceSpace('local'), + ]).then((allSpaces) => new Promise((resolve, reject) => { + const [ + localSpace1, + viewerSpace, + localFloorSpace, + boundedFloorSpace, + unboundedSpace, + localSpace2] = allSpaces; + + const offsetLocalSpace1 = localSpace1.getOffsetReferenceSpace(offsetSpaceTransform); + const offsetLocalSpace2 = localSpace2.getOffsetReferenceSpace(offsetSpaceTransform); + const offsetViewerSpace = viewerSpace.getOffsetReferenceSpace(offsetSpaceTransform); + + // Throw in an offset space: + allSpaces.push(offsetLocalSpace1); + + function onFrame(time, frame) { + // Expect identities: + const viewerFromViewer1 = frame.getViewerPose(viewerSpace); + + const localFromLocalDifferentAddress = frame.getPose(localSpace1, localSpace2); + const offsetFromOffsetDifferentAddress = frame.getPose(offsetLocalSpace1, offsetLocalSpace2); + + t.step(() => { + expectIdentity(viewerFromViewer1, "viewerFromViewer1"); + expectIdentity(localFromLocalDifferentAddress, "localFromLocalDifferentAddress"); + expectIdentity(offsetFromOffsetDifferentAddress, "offsetFromOffsetDifferentAddress"); + }); + + // Expect offsetSpaceTransform: + const viewerFromViewer2 = frame.getViewerPose(offsetViewerSpace); + + const viewerFromOffset2 = frame.getPose(offsetViewerSpace, viewerSpace); + const localFromOffset1 = frame.getPose(offsetLocalSpace1, localSpace1); + const localFromOffset2 = frame.getPose(offsetLocalSpace2, localSpace1); + + t.step(() => { + expectMatrix(viewerFromViewer2, offsetSpaceTransform.matrix, "viewerFromViewer2"); + expectMatrix(viewerFromOffset2, offsetSpaceTransform.matrix, "viewerFromOffset2"); + expectMatrix(localFromOffset1, offsetSpaceTransform.matrix, "localFromOffset1"); + expectMatrix(localFromOffset2, offsetSpaceTransform.matrix, "localFromOffset2"); + }); + + // Expect identities: + allSpaces.forEach((space, index) => { + const pose = frame.getPose(space, space); + t.step(() => { + expectIdentity(pose, "pose[" + index +"]"); + }); + }); + + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); + }; + + xr_session_promise_test(testName, testFunction, + deviceInitParams, 'immersive-vr', { + requiredFeatures: ["local", "local-floor", "bounded-floor", "unbounded"] + }); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrFrame_lifetime.https.html b/testing/web-platform/tests/webxr/xrFrame_lifetime.https.html new file mode 100644 index 0000000000..e457ef020f --- /dev/null +++ b/testing/web-platform/tests/webxr/xrFrame_lifetime.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + let immersiveTestName = "XRFrame methods throw exceptions outside of the " + + "requestAnimationFrame callback for immersive sessions"; + let nonImmersiveTestName = "XRFrame methods throw exceptions outside of the " + + "requestAnimationFrame callback for non-immersive sessions"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = (testSession, testController, t) => new Promise((resolve) => { + let staleFrame = null; + let currentReferenceSpace = null; + let currentViewerSpace = null; + + function onFrame(time, xrFrame) { + t.step(() => { + assert_true(xrFrame instanceof XRFrame); + }); + + staleFrame = xrFrame; + step_timeout(afterFrame, 0); + } + + function afterFrame() { + t.step(() => { + // Attempting to call a method on the frame outside the callback that + // originally provided it should cause it to throw an exception. + assert_throws_dom('InvalidStateError', () => staleFrame.getViewerPose(currentReferenceSpace)); + assert_throws_dom('InvalidStateError', () => staleFrame.getPose(currentViewerSpace, currentReferenceSpace)); + }); + + // Test does not complete until the this function has executed. + resolve(); + } + + testSession.requestReferenceSpace('viewer').then((viewerSpace) => { + currentViewerSpace = viewerSpace; + testSession.requestReferenceSpace('local').then((referenceSpace) => { + currentReferenceSpace = referenceSpace; + testSession.requestAnimationFrame(onFrame); + }); + }); + }); + + xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline', { 'requiredFeatures': ['local'] }); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrFrame_session_sameObject.https.html b/testing/web-platform/tests/webxr/xrFrame_session_sameObject.https.html new file mode 100644 index 0000000000..d1d4beb164 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrFrame_session_sameObject.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRFrame.session meets [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + session.requestAnimationFrame((time, xrFrame) => { + let session = xrFrame.session; + t.step(() => { + assert_equals(session, xrFrame.session, + "XRFrame.session returns the same object."); + }); + resolve(); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrInputSource_add_remove.https.html b/testing/web-platform/tests/webxr/xrInputSource_add_remove.https.html new file mode 100644 index 0000000000..0025331198 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrInputSource_add_remove.https.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +let testName = "XRInputSources can be properly added and removed from the " + + "session"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = (session, fakeDeviceController, t) => new Promise((resolve) => { + let input_sources = session.inputSources; + + t.step( () => { + assert_equals(input_sources.length, 0); + }); + + let input_source_1 = fakeDeviceController.simulateInputSourceConnection(RIGHT_CONTROLLER); + + requestSkipAnimationFrame(session, (time, xrFrame) => { + let input_sources = session.inputSources; + + t.step( () => { + assert_equals(input_sources.length, 1); + assert_equals(input_sources[0].targetRayMode, "tracked-pointer"); + assert_equals(input_sources[0].handedness, "right"); + }); + + let input_source_2 = fakeDeviceController.simulateInputSourceConnection({ + handedness: "none", + targetRayMode: "gaze", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [] + }); + + session.requestAnimationFrame((time, xrFrame) => { + let input_sources = session.inputSources; + + t.step( () => { + assert_equals(input_sources.length, 2); + assert_equals(input_sources[1].targetRayMode, "gaze"); + assert_equals(input_sources[1].handedness, "none"); + }); + + input_source_1.disconnect(); + + session.requestAnimationFrame((time, xrFrame) => { + let input_sources = session.inputSources; + + t.step( () => { + assert_equals(input_sources.length, 1); + assert_equals(input_sources[0].targetRayMode, "gaze"); + assert_equals(input_sources[0].handedness, "none"); + }); + + resolve(); + }); + }); + }); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrInputSource_emulatedPosition.https.html b/testing/web-platform/tests/webxr/xrInputSource_emulatedPosition.https.html new file mode 100644 index 0000000000..8a4aec48a8 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrInputSource_emulatedPosition.https.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let testName = "Poses from XRInputSource.gripSpace have emulatedPosition set " + + "properly"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: IDENTITY_TRANSFORM, + gripOrigin: VALID_GRIP_TRANSFORM, + profiles: [] + }); + + // Must have a reference space to get input poses. eye-level doesn't apply + // any transforms to the given matrix. + session.requestReferenceSpace('local').then( (referenceSpace) => { + + function CheckPositionNotEmulated(time, xrFrame) { + let source = session.inputSources[0]; + let grip_pose = xrFrame.getPose(source.gripSpace, referenceSpace); + + t.step(() => { + assert_not_equals(grip_pose, null); + assert_equals(grip_pose.emulatedPosition, false); + }); + + input_source.setGripOrigin(VALID_GRIP_TRANSFORM, true); + session.requestAnimationFrame(CheckPositionEmulated); + } + + function CheckPositionEmulated(time, xrFrame) { + let source = session.inputSources[0]; + let grip_pose = xrFrame.getPose(source.gripSpace, referenceSpace); + + t.step(() => { + assert_not_equals(grip_pose, null); + assert_equals(grip_pose.emulatedPosition, true); + }); + + resolve(); + } + + // Can only request input poses in an xr frame. + requestSkipAnimationFrame(session, CheckPositionNotEmulated); + }); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrInputSource_getPose_targetRay_grip.https.html b/testing/web-platform/tests/webxr/xrInputSource_getPose_targetRay_grip.https.html new file mode 100644 index 0000000000..bac9e90494 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrInputSource_getPose_targetRay_grip.https.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +const testName = "Poses between targetRaySpace and gripSpace can be obtained and behave correctly"; + +const fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +const testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + const input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: IDENTITY_TRANSFORM, // basespace_from_target_ray_space + gripOrigin: VALID_GRIP_TRANSFORM, // basespace_from_grip_space + profiles: [] + }); + + function CheckPoseBetweenTargetRayAndGripSpaces(time, xrFrame) { + let source = session.inputSources[0]; + let target_ray_space_from_grip_space = xrFrame.getPose(source.gripSpace, source.targetRaySpace); + // target_ray_space_from_grip_space + // = (basespace_from_target_ray_space)^-1 * basespace_from_grip_space + // + // substituting identity for basespace_from_target_ray_space: + // = (identity)^-1 * basespace_from_grip_space + // = basespace_from_grip_space + // = VALID_GRIP_TRANSFORM, whose matrix is equal to VALID_GRIP + + t.step(() => { + assert_not_equals(target_ray_space_from_grip_space, null); + assert_matrix_approx_equals(target_ray_space_from_grip_space.transform.matrix, VALID_GRIP); + }); + + resolve(); + } + + // Can only request input poses in an xr frame. + requestSkipAnimationFrame(session, CheckPoseBetweenTargetRayAndGripSpaces); + }); // new Promise((resolve) => { + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrInputSource_profiles.https.html b/testing/web-platform/tests/webxr/xrInputSource_profiles.https.html new file mode 100644 index 0000000000..de70c9058f --- /dev/null +++ b/testing/web-platform/tests/webxr/xrInputSource_profiles.https.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "WebXR InputSource's profiles list can be set"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: ["most-specific-name", "less-specific-name"] + }); + + // Input events and state changes need one frame to propagate, which is why we + // are requesting an animation frame before checking the profiles list. + return new Promise((resolve) => { + requestSkipAnimationFrame(session, () => { + let profiles = session.inputSources[0].profiles; + t.step(() => { + assert_equals(profiles.length, 2); + assert_equals(profiles[0], "most-specific-name"); + assert_equals(profiles[1], "less-specific-name"); + }, "Verify profiles list is set"); + resolve(); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrInputSource_sameObject.https.html b/testing/web-platform/tests/webxr/xrInputSource_sameObject.https.html new file mode 100644 index 0000000000..999b4d82b6 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrInputSource_sameObject.https.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRInputSource attributes meet [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + gripOrigin: VALID_GRIP_TRANSFORM, + profiles: ["foo", "bar"] + }); + + requestSkipAnimationFrame(session, (time, xrFrame) => { + let source = session.inputSources[0]; + let targetRaySpace = source.targetRaySpace; + let gripSpace = source.gripSpace; + let profiles = source.profiles; + + t.step(() => { + assert_not_equals(targetRaySpace, null, + "target ray space must not be null"); + assert_not_equals(gripSpace, null, "grip space must not be null"); + + // Make sure [SameObject] attributes actually have the same object + // returned each time they are accessed. + assert_equals(targetRaySpace, source.targetRaySpace, + "XRInputSource.targetRaySpace returns the same object."); + assert_equals(gripSpace, source.gripSpace, + "XRInputSource.gripSpace returns the same object."); + assert_equals(profiles, source.profiles, + "XRInputSource.profiles returns the same object."); + }); + + session.requestAnimationFrame((time, xrFrame) => { + // Make sure the attributes still return the same object on the next + // frame when nothing has happened that would cause the input source + // to be recreated. + t.step(() => { + assert_equals(targetRaySpace, source.targetRaySpace, + "XRInputSource.targetRaySpace returns the same object each frame."); + assert_equals(gripSpace, source.gripSpace, + "XRInputSource.gripSpace returns the same object each frame."); + assert_equals(profiles, source.profiles, + "XRInputSource.profiles returns the same object each frame."); + }); + + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrPose_transform_sameObject.https.html b/testing/web-platform/tests/webxr/xrPose_transform_sameObject.https.html new file mode 100644 index 0000000000..9ea07fcd3b --- /dev/null +++ b/testing/web-platform/tests/webxr/xrPose_transform_sameObject.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRPose.transform meets [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + gripOrigin: VALID_GRIP_TRANSFORM, + profiles: [] + }); + + session.requestReferenceSpace('local').then((referenceSpace) => { + requestSkipAnimationFrame(session, (time, xrFrame) => { + let source = session.inputSources[0]; + let input_pose = xrFrame.getPose(source.targetRaySpace, referenceSpace); + + // Make sure that the transform attribute is the same object each time + // we access it. This verifies that the XRPose does *not* do something + // spec-noncompliant such as creating and returning a new + // XRRigidTransform object each time the attribute is accessed. + let transform = input_pose.transform; + t.step(() => { + assert_equals(transform, input_pose.transform, + "XRPose.transform returns the same object."); + }); + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset.https.html b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset.https.html new file mode 100644 index 0000000000..d93f696a2d --- /dev/null +++ b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset.https.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> +let testName = "Updating XRReferenceSpace origin offset updates view and input matrices."; + +const VIEW_OFFSET_WITH_ROTATION = { + position: [4, 3, 2], + orientation: [0, -0.7071, 0, 0.7071 ] +}; + +const VIEWS_WITH_OFFSET = [{ + eye:"left", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: VIEW_OFFSET_WITH_ROTATION, + resolution: VALID_RESOLUTION +}, { + eye:"right", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: VIEW_OFFSET_WITH_ROTATION, + resolution: VALID_RESOLUTION +}]; + +let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + viewerOrigin: VALID_POSE_TRANSFORM, + views: VIEWS_WITH_OFFSET, + supportedFeatures: ALL_FEATURES +}; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + const GRIP_TRANSFORM_WITH_ROTATION = { + position: [1, 2, 3], + orientation: [0, 0.7071, 0, 0.7071] + }; + + const POINTER_TRANSFORM_WITH_ROTATION = { + position: [0, 1, 4], + orientation: [-0.5, 0.5, 0.5, -0.5] + }; + + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: POINTER_TRANSFORM_WITH_ROTATION, + gripOrigin: GRIP_TRANSFORM_WITH_ROTATION, + profiles: [] + }); + + const RADIANS_90D = Math.PI / 2; + + const EXPECTED_VIEW_MATRIX_1 = [1, 0, 0, 0, 0, 0, -1, 0, 0, 1, 0, 0, -3, -4, 5, 1]; + const EXPECTED_GRIP_MATRIX_1 = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 2, 3, 1]; + const EXPECTED_RAY_MATRIX_1 = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 2, 2, 1, 1]; + + const EXPECTED_VIEW_MATRIX_2 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 7, 1, 8, 1]; + const EXPECTED_GRIP_MATRIX_2 = [0, -1, 0, 0, 0, 0, -1, 0, 1, 0, 0, 0, -9, -2, -5, 1]; + const EXPECTED_RAY_MATRIX_2 = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, -8, -4, -5, 1]; + + const EXPECTED_VIEW_MATRIX_3 = [0, 0, -1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 12, 3, 8, 1]; + const EXPECTED_GRIP_MATRIX_3 = [0, -1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 5, -4, -14, 1]; + const EXPECTED_RAY_MATRIX_3 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 5, -6, -13, 1]; + + // Must have a reference space to get input poses. eye-level doesn't apply + // any transforms to the given matrix. + session.requestReferenceSpace('local').then( (referenceSpace) => { + function OnFrame(time, frame) { + let source = session.inputSources[0]; + + function CheckState(referenceSpace, expected_view_matrix, expected_grip_matrix, expected_ray_matrix, prefix) { + t.step(() => { + let pose = frame.getViewerPose(referenceSpace); + let grip_pose = frame.getPose(source.gripSpace, referenceSpace); + let input_pose = frame.getPose(source.targetRaySpace, referenceSpace); + + let view_matrix = pose.views[0].transform.inverse.matrix; + let grip_matrix = grip_pose.transform.matrix; + let ray_matrix = input_pose.transform.matrix; + + assert_matrix_approx_equals(view_matrix, expected_view_matrix, prefix + " view matrix"); + assert_matrix_approx_equals(grip_matrix, expected_grip_matrix, prefix + " grip matrix"); + assert_matrix_approx_equals(ray_matrix, expected_ray_matrix, prefix + " ray matrix"); + }); + } + + CheckState(referenceSpace, EXPECTED_VIEW_MATRIX_1, EXPECTED_GRIP_MATRIX_1, EXPECTED_RAY_MATRIX_1, "Initial"); + + const new_position1 = { + x: 10, + y: -3, + z: 5, + }; + const new_orientation1 = { + x: Math.sin(RADIANS_90D / 2), + y: 0, + z: 0, + w: Math.cos(RADIANS_90D / 2), + }; + + referenceSpace = referenceSpace.getOffsetReferenceSpace(new XRRigidTransform(new_position1, new_orientation1)); + CheckState(referenceSpace, EXPECTED_VIEW_MATRIX_2, EXPECTED_GRIP_MATRIX_2, EXPECTED_RAY_MATRIX_2, "First transform"); + + const new_position2 = { + x: 5, + y: 2, + z: 0, + }; + const new_orientation2 = { + x: 0, + y: Math.sin(RADIANS_90D / 2), + z: 0, + w: Math.cos(RADIANS_90D / 2), + }; + + referenceSpace = referenceSpace.getOffsetReferenceSpace(new XRRigidTransform(new_position2, new_orientation2)); + CheckState(referenceSpace, EXPECTED_VIEW_MATRIX_3, EXPECTED_GRIP_MATRIX_3, EXPECTED_RAY_MATRIX_3, "Second transform"); + + resolve(); + } + + // Can only request input poses in an xr frame. + requestSkipAnimationFrame(session, OnFrame); + }); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrReferenceSpace_originOffsetBounded.https.html b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffsetBounded.https.html new file mode 100644 index 0000000000..fd961735ec --- /dev/null +++ b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffsetBounded.https.html @@ -0,0 +1,244 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +const testName = "Updating XRBoundedReferenceSpace origin offset updates view, input matrices, and bounds geometry."; + +const INITIAL_VIEW_OFFSET = { + position: [1, 2, 3], + orientation: [0,0,0,1] +}; + +const VIEWS_WITH_OFFSET = [{ + eye:"left", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: INITIAL_VIEW_OFFSET, + resolution: VALID_RESOLUTION + }, { + eye:"right", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: INITIAL_VIEW_OFFSET, + resolution: VALID_RESOLUTION +}]; + +const FLOOR_TRANSFORM = { + position: [-0.1, -0.2, -0.3], + orientation: [0, 0, 0, 1] +}; + +const fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VIEWS_WITH_OFFSET, + viewerOrigin: IDENTITY_TRANSFORM, + floorOrigin: FLOOR_TRANSFORM, + supportedFeatures: ALL_FEATURES, + boundsCoordinates: [ + { x: 1, z: -1.5 }, + { x: 1, z: 1.5 }, + { x: -1, z: 1.5 }, + { x: -1, z: -1.5 } + ] +}; + +function testFunction(session, fakeDeviceController, t) { + const INITIAL_GRIP_TRANSFORM = { + position: [1, 2, 3], + orientation: [0, 0, 0, 1] + }; + + const LOCAL_POINTER_TRANSFORM = { + position: [1.01, 2.02, 3.03], + orientation: [0, 0, 0, 1] + } + + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: LOCAL_POINTER_TRANSFORM, + gripOrigin: INITIAL_GRIP_TRANSFORM, + profiles: [] + }); + + return new Promise((resolve, reject) => { + session.requestReferenceSpace('bounded-floor').then((referenceSpace) => { + requestSkipAnimationFrame(session, (time, frame) => { + let input_source = session.inputSources[0]; + + function CheckState( + reference_space, + expected_view_matrix, + expected_grip_matrix, + expected_ray_matrix, + expected_bounds_geometry + ) { + t.step(() => { + let pose = frame.getViewerPose(reference_space); + let grip_pose = frame.getPose(input_source.gripSpace, reference_space); + let input_pose = frame.getPose(input_source.targetRaySpace, reference_space); + + let view_matrix = pose.views[0].transform.inverse.matrix; + let grip_matrix = grip_pose.transform.matrix; + let ray_matrix = input_pose.transform.matrix; + + assert_matrix_approx_equals(view_matrix, expected_view_matrix); + assert_matrix_approx_equals(grip_matrix, expected_grip_matrix); + assert_matrix_approx_equals(ray_matrix, expected_ray_matrix); + + assert_equals(reference_space.boundsGeometry.length, expected_bounds_geometry.length); + for (var i = 0; i < reference_space.boundsGeometry.length; ++i) { + assert_point_approx_equals(reference_space.boundsGeometry[i], expected_bounds_geometry[i]); + } + }); + } + + const EXPECTED_VIEW_MATRIX_1 = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + -1.1, -2.2, -3.3, 1, + ]; + const EXPECTED_GRIP_MATRIX_1 = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1.1, 2.2, 3.3, 1, + ]; + const EXPECTED_RAY_MATRIX_1 = [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 1.11, 2.22, 3.33, 1, + ]; + + const EXPECTED_BOUNDS_GEOMETRY_1 = [ + {x: 1, y: 0, z: -1.5, w: 1}, + {x: 1, y: 0, z: 1.5, w: 1}, + {x: -1, y: 0, z: 1.5, w: 1}, + {x: -1, y: 0, z: -1.5, w: 1}, + ]; + + // Check state after initialization + CheckState( + referenceSpace, + EXPECTED_VIEW_MATRIX_1, + EXPECTED_GRIP_MATRIX_1, + EXPECTED_RAY_MATRIX_1, + EXPECTED_BOUNDS_GEOMETRY_1 + ); + + const RADIANS_90D = Math.PI / 2; + + // Perform arbitrary transformation to reference space originOffset + const new_position1 = { + x: 10, // Translate 10 units along the x-axis + y: -3, // Translate -3 units along the y-axis + z: 5, // Translate 5 units along the z-axis + }; + const new_orientation1 = { + x: Math.sin(RADIANS_90D / 2), // Rotate 90 degrees around the x-axis + y: 0, + z: 0, + w: Math.cos(RADIANS_90D / 2), + }; + referenceSpace = referenceSpace.getOffsetReferenceSpace(new XRRigidTransform(new_position1, new_orientation1)); + + const EXPECTED_VIEW_MATRIX_2 = [ + 1, 0, 0, 0, + 0, 0, 1, 0, + 0, -1, 0, 0, + 8.9, -5.2, 1.7, 1, + ]; + const EXPECTED_GRIP_MATRIX_2 = [ + 1, 0, 0, 0, + 0, 0, -1, 0, + 0, 1, 0, 0, + -8.9, -1.7, -5.2, 1, + ]; + const EXPECTED_RAY_MATRIX_2 = [ + 1, 0, 0, 0, + 0, 0, -1, 0, + 0, 1, 0, 0, + -8.89, -1.67, -5.22, 1, + ]; + + const EXPECTED_BOUNDS_GEOMETRY_2 = [ + {x: -9, y: -6.5, z: -3, w: 1}, + {x: -9, y: -3.5, z: -3, w: 1}, + {x: -11, y: -3.5, z: -3, w: 1}, + {x: -11, y: -6.5, z: -3, w: 1}, + ]; + + // Check state after transformation + CheckState( + referenceSpace, + EXPECTED_VIEW_MATRIX_2, + EXPECTED_GRIP_MATRIX_2, + EXPECTED_RAY_MATRIX_2, + EXPECTED_BOUNDS_GEOMETRY_2 + ); + + // Perform arbitrary transformation to reference space originOffset + const new_position2 = { + x: 5, // Translate 5 units along the x-axis + y: 2, // Translate 2 units along the y-axis + z: 0, + }; + const new_orientation2 = { + x: 0, + y: Math.sin(RADIANS_90D / 2), // Rotate 90 degrees about the y-axis + z: 0, + w: Math.cos(RADIANS_90D / 2), + }; + referenceSpace = referenceSpace.getOffsetReferenceSpace(new XRRigidTransform(new_position2, new_orientation2)); + + const EXPECTED_VIEW_MATRIX_3 = [ + 0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + 13.9, -5.2, 3.7, 1, + ]; + const EXPECTED_GRIP_MATRIX_3 = [ + 0, 0, 1, 0, + 1, 0, 0, 0, + 0, 1, 0, 0, + 5.2, -3.7, -13.9, 1, + ]; + const EXPECTED_RAY_MATRIX_3 = [ + 0, 0, 1, 0, + 1, 0, 0, 0, + 0, 1, 0, 0, + 5.22, -3.67, -13.89, 1, + ]; + + const EXPECTED_BOUNDS_GEOMETRY_3 = [ + {x: 3, y: -8.5, z: -14, w: 1}, + {x: 3, y: -5.5, z: -14, w: 1}, + {x: 3, y: -5.5, z: -16, w: 1}, + {x: 3, y: -8.5, z: -16, w: 1}, + ]; + + // Check state after transformation + CheckState( + referenceSpace, + EXPECTED_VIEW_MATRIX_3, + EXPECTED_GRIP_MATRIX_3, + EXPECTED_RAY_MATRIX_3, + EXPECTED_BOUNDS_GEOMETRY_3 + ); + + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr', { 'requiredFeatures': ['bounded-floor'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset_viewer.https.html b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset_viewer.https.html new file mode 100644 index 0000000000..8a9fb344a3 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrReferenceSpace_originOffset_viewer.https.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> +let testName = "Creating XRReferenceSpace origin offset off of `viewer` space works."; + +let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + viewerOrigin: VALID_POSE_TRANSFORM, + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES +}; + +let originOffsetPosition = new DOMPointReadOnly(0, 0, 1); + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve) => { + + session.requestReferenceSpace('local').then( (localSpace) => { + session.requestReferenceSpace('viewer').then( (viewerSpace) => { + + let offsetSpace = viewerSpace.getOffsetReferenceSpace( + new XRRigidTransform(originOffsetPosition)); + + function OnFrame(time, frame) { + let viewerPose = frame.getPose(viewerSpace, localSpace); + let offsetPose = frame.getPose(offsetSpace, localSpace); + + let viewerPose2 = frame.getPose(localSpace ,viewerSpace); + let offsetPose2 = frame.getPose(localSpace, offsetSpace); + + t.step(() => { + assert_point_significantly_not_equals(viewerPose.transform.position, offsetPose.transform.position); + assert_point_significantly_not_equals(viewerPose2.transform.position, offsetPose2.transform.position); + }); + + resolve(); + } + + // Can only request input poses in an xr frame. + session.requestAnimationFrame(OnFrame); + }); + }); + }); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrReferenceSpace_relationships.https.html b/testing/web-platform/tests/webxr/xrReferenceSpace_relationships.https.html new file mode 100644 index 0000000000..5b680fb861 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrReferenceSpace_relationships.https.html @@ -0,0 +1,58 @@ + +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> +<script> +let testName = + "Bounded space, viewer space, local and local-floor space have correct poses w.r.t. each other"; +// 1m above world origin. +const VIEWER_ORIGIN_TRANSFORM = { + position: [0, 1, 0], + orientation: [0, 0, 0, 1], +}; +// 0.25m above world origin. +const FLOOR_ORIGIN_TRANSFORM = { + position: [0, 0.25, 0], + orientation: [0, 0, 0, 1], +}; +const fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: VIEWER_ORIGIN_TRANSFORM, + floorOrigin: FLOOR_ORIGIN_TRANSFORM, + supportedFeatures: ALL_FEATURES +}; +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve, reject) => { + Promise.all([ + session.requestReferenceSpace('bounded-floor'), + session.requestReferenceSpace('local'), + session.requestReferenceSpace('local-floor'), + session.requestReferenceSpace('viewer') + ]).then(([boundedSpace, localSpace, localFloorSpace, viewerSpace]) => { + t.step(() => { + }); + const onFrame = function(time, xrFrame) { + const localFloorPoseInLocalSpace = xrFrame.getPose(localFloorSpace, localSpace); + const viewerPoseInLocalFloorSpace = xrFrame.getPose(viewerSpace, localFloorSpace); + const boundedFloorPoseInLocalFloorSpace = xrFrame.getPose(boundedSpace, localFloorSpace); + t.step(() => { + // Local floor space is supposed to be 0.25m above local space (aka world space). + assert_equals(localFloorPoseInLocalSpace.transform.position.y, 0.25); + // Bounded floor space should be at the same height as local floor space. + assert_equals(boundedFloorPoseInLocalFloorSpace.transform.position.y, 0.0); + // Viewer space should be additional 0.75m above local-floor space. + assert_equals(viewerPoseInLocalFloorSpace.transform.position.y, 0.75); + }); + resolve(); + } + session.requestAnimationFrame(onFrame); + }); + }); +}; +xr_session_promise_test(testName, testFunction, fakeDeviceInitParams, 'immersive-vr', { 'requiredFeatures': ['local-floor', 'bounded-floor'] }); +</script> diff --git a/testing/web-platform/tests/webxr/xrRigidTransform_constructor.https.html b/testing/web-platform/tests/webxr/xrRigidTransform_constructor.https.html new file mode 100644 index 0000000000..01e069c80b --- /dev/null +++ b/testing/web-platform/tests/webxr/xrRigidTransform_constructor.https.html @@ -0,0 +1,148 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script> + +let testName = "XRRigidTransform constructor works"; +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve, reject) => { + let coordDict = function(coords) { + let tempDict = { + x : coords[0], + y : coords[1], + z : coords[2] + } + + if (coords.length >= 4) { + tempDict["w"] = coords[3]; + } + + return tempDict; + }; + + let createDOMPoint = function(coords) { + return DOMPoint.fromPoint(coordDict(coords)); + }; + + let createDOMPointReadOnly = function(coords) { + return DOMPointReadOnly.fromPoint(coordDict(coords)); + }; + + let quaternionLength = function(point) { + return Math.sqrt( + (point.x * point.x) + + (point.y * point.y) + + (point.z * point.z) + + (point.w * point.w)); + }; + + let checkDOMPoint = function(point, x, y, z, w, desc) { + t.step(() => { + assert_approx_equals(point.x, x, FLOAT_EPSILON, `${desc}: x value`); + assert_approx_equals(point.y, y, FLOAT_EPSILON, `${desc}: y value`); + assert_approx_equals(point.z, z, FLOAT_EPSILON, `${desc}: z value`); + assert_approx_equals(point.w, w, FLOAT_EPSILON, `${desc}: w value`); + }); + }; + + let checkTransform = function(transformObj, desc) { + t.step(() => { + assert_not_equals(transformObj, null, `${desc}: exists`); + assert_not_equals(transformObj.position, null, `${desc}: position exists`); + assert_not_equals(transformObj.orientation, null, `${desc}: orientation exists`); + assert_not_equals(transformObj.matrix, null, `${desc}: matrix exists`); + assert_equals(transformObj.matrix.length, 16, `${desc}: matrix of correct length`); + }); + }; + + // test creating transform with specified position and orientation + // make sure that orientation was normalized to have length = 1.0 + let transform = new XRRigidTransform( + createDOMPoint([1.0, 2.0, 3.0]), + createDOMPoint([1.1, 2.1, 3.1, 1.0])); + checkTransform(transform, "Arbitrary transform"); + checkDOMPoint(transform.position, 1.0, 2.0, 3.0, 1.0, "Arbitrary transform position"); + assert_approx_equals(quaternionLength(transform.orientation), 1.0, FLOAT_EPSILON, + "Arbitrary transform is normalized"); + + // test creating identity transform + let identity = new XRRigidTransform(); + checkTransform(identity, "Identity transform"); + checkDOMPoint(identity.position, 0.0, 0.0, 0.0, 1.0, "Identity transform position"); + checkDOMPoint(identity.orientation, 0.0, 0.0, 0.0, 1.0, "Identity transform orientation"); + + // create transform with only position specified + transform = new XRRigidTransform(createDOMPoint([1.0, 2.0, 3.0])); + checkTransform(transform, "Position-only"); + + // create transform with only orientation specified + transform = new XRRigidTransform(undefined, createDOMPoint([1.1, 2.1, 3.1, 1.0])); + checkTransform(transform, "orientation-only"); + + // create transform with DOMPointReadOnly + transform = new XRRigidTransform( + createDOMPointReadOnly([1.0, 2.0, 3.0]), + createDOMPointReadOnly([1.1, 2.1, 3.1, 1.0])); + checkTransform(transform, "Created with DOMPointReadOnly"); + + // create transform with dictionary + transform = new XRRigidTransform( + coordDict([1.0, 2.0, 3.0]), + coordDict([1.1, 2.1, 3.1, 1.0])); + checkTransform(transform, "Created with dict"); + + assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 0.5]), + coordDict([1.1, 2.1, 3.1, 1.0]) + ), "Constructor should throw TypeError for non-1 position w values"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([NaN, 2.0, 3.0, 1.0]), + coordDict([1.1, 2.1, 3.1, 1.0]) +), "Constructor should throw TypeError if position values contain NaN"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, Infinity, 3.0, 1.0]), + coordDict([1.1, 2.1, 3.1, 1.0]) +), "Constructor should throw TypeError if position values contain Infinity"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, 2.0, -Infinity, 1.0]), + coordDict([1.1, 2.1, 3.1, 1.0]) +), "Constructor should throw TypeError if position values contain -Infinity"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 1.0]), + coordDict([NaN, 2.1, 3.1, 1.0]) +), "Constructor should throw TypeError if orientation values contain NaN"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 1.0]), + coordDict([1.1, Infinity, 3.1, 1.0]) +), "Constructor should throw TypeError if orientation values contain Infinity"); + +assert_throws_js(TypeError, () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 1.0]), + coordDict([1.1, 2.1, -Infinity, 1.0]) +), "Constructor should throw TypeError if orientation values contain -Infinity"); + + assert_throws_dom("InvalidStateError", () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 1.0]), + coordDict([0, 0, 0, 0]) + ), "Constructor should throw InvalidStateError for non-normalizeable orientation values"); + +assert_throws_dom("InvalidStateError", () => new XRRigidTransform( + coordDict([1.0, 2.0, 3.0, 1.0]), + coordDict([-1.7976931348623157e+308, 0, 0, 0]) +), "Constructor should throw InvalidStateError for non-normalizeable orientation values"); + resolve(); +}); + +xr_session_promise_test(testName, testFunction, fakeDeviceInitParams, + 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrRigidTransform_inverse.https.html b/testing/web-platform/tests/webxr/xrRigidTransform_inverse.https.html new file mode 100644 index 0000000000..d4fdc15396 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrRigidTransform_inverse.https.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_math_utils.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> +<script> + +let testName = "XRRigidTransform inverse works"; +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve, reject) => { + + // An identity transform should be equal to it's inverse. + let identity_transform = new XRRigidTransform(); + t.step(() => { + assert_transform_approx_equals(identity_transform, identity_transform.inverse); + }); + + // Inversed transforms should yield the expected results + let transform = new XRRigidTransform( + { x: 1.0, y: 2.0, z: 3.0 }, + { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }); + let inverse_transform = transform.inverse; + let expected_inverse = new XRRigidTransform( + { x: -1.0, y: -2.0, z: -3.0 }, + { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }); + t.step(() => { + assert_transform_approx_equals(inverse_transform, expected_inverse); + }); + + transform = new XRRigidTransform( + { x: 0.0, y: 0.0, z: 0.0 }, + { x: 1.0, y: 0.0, z: 0.0, w: 1.0 }); + inverse_transform = transform.inverse; + expected_inverse = new XRRigidTransform( + { x: 0.0, y: 0.0, z: 0.0 }, + { x: -1.0, y: 0.0, z: 0.0, w: 1.0 }); + t.step(() => { + assert_transform_approx_equals(inverse_transform, expected_inverse); + }); + + transform = new XRRigidTransform( + { x: 1.0, y: 2.0, z: 3.0 }, + { x: 0.0, y: 1.0, z: 0.0, w: 1.0 }); + inverse_transform = transform.inverse; + expected_inverse = new XRRigidTransform( + { x: 3.0, y: -2.0, z: -1.0 }, + { x: 0.0, y: -1.0, z: 0.0, w: 1.0 }); + t.step(() => { + assert_transform_approx_equals(inverse_transform, expected_inverse); + }); + + // Transforms should be equal to the inverse of their inverse. + transform = new XRRigidTransform( + { x: 1.0, y: 2.0, z: 3.0 }, + { x: 1.0, y: 2.0, z: 3.0, w: 4.0 }); + inverse_transform = transform.inverse; + t.step(() => { + assert_transform_approx_equals(transform, inverse_transform.inverse); + }); + + transform = new XRRigidTransform( + { x: -9.0, y: 8.0, z: -7.0 }, + { x: 6.0, y: -5.0, z: 4.0, w: 3.0 }); + inverse_transform = transform.inverse; + t.step(() => { + assert_transform_approx_equals(transform, inverse_transform.inverse); + }); + + transform = new XRRigidTransform( + { x: -2.0, y: 1.0, z: -4.0 }, + { x: 0.0, y: 1.0, z: 0.0, w: 1.0 }); + inverse_transform = transform.inverse; + t.step(() => { + assert_transform_approx_equals(transform, inverse_transform.inverse); + }); + + transform = new XRRigidTransform( + { x: 2.0, y: -1.0, z: 4.0 }, + { x: 1.0, y: 0.0, z: 0.0, w: 1.0 }); + inverse_transform = transform.inverse; + t.step(() => { + assert_transform_approx_equals(transform, inverse_transform.inverse); + }); + + // Inverse should always return the same object, and calling inverse on that + // object should return the original object. + transform = new XRRigidTransform( + { x: 1.0, y: -2.0, z: 3.0 }, + { x: 0.0, y: 0.0, z: 1.0, w: 1.0 }); + inverse_transform = transform.inverse; + t.step(() => { + assert_equals(transform.inverse, inverse_transform); + assert_equals(inverse_transform.inverse, transform); + assert_equals(transform.inverse.inverse, transform); + assert_equals(transform.inverse.inverse.inverse, inverse_transform); + }); + + resolve(); +}); + +xr_session_promise_test(testName, testFunction, fakeDeviceInitParams, + 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrRigidTransform_matrix.https.html b/testing/web-platform/tests/webxr/xrRigidTransform_matrix.https.html new file mode 100644 index 0000000000..df804193ff --- /dev/null +++ b/testing/web-platform/tests/webxr/xrRigidTransform_matrix.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> +<script src="resources/webxr_math_utils.js"></script> +<script> + +let matrix_tests_name = "XRRigidTransform matrix works"; + +let matrix_tests = function() { + // Matrix tests for XRRigidTransform. + + // Test 1. Check if matrix rotates the vector the same way as quaternion + // used to construct it. This does not perform a translation. + { + // point + const originDict = {x : 0, y : 0, z : 0, w : 1}; + // quaternion - should be normalized + const orientationDict = {x : 0, y : 0.3805356, z : 0.7610712, w : 0.525322 } + + let rigidTransform = new XRRigidTransform( + DOMPoint.fromPoint(originDict), + DOMPoint.fromPoint(orientationDict)); + + const point_0 = {x : 10, y : 11, z : 12, w : 1}; + + // transform the point by matrix from rigid transform + const point_transformed_0 = normalize_perspective(transform_point_by_matrix(rigidTransform.matrix, point_0)); + const point_transformed_1 = transform_point_by_quaternion(orientationDict, point_0); + const point_transformed_2 = transform_point_by_quaternion(rigidTransform.orientation, point_0); + + assert_point_approx_equals( + point_transformed_1, point_transformed_0, + FLOAT_EPSILON, "by-initial-quaternion-and-matrix:"); + + assert_point_approx_equals( + point_transformed_2, point_transformed_0, + FLOAT_EPSILON, "by-transform's-quaternion-and-matrix:"); + } +}; + +test(matrix_tests, matrix_tests_name); + +</script> diff --git a/testing/web-platform/tests/webxr/xrRigidTransform_sameObject.https.html b/testing/web-platform/tests/webxr/xrRigidTransform_sameObject.https.html new file mode 100644 index 0000000000..8ff4f5c258 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrRigidTransform_sameObject.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script> + +let testName = "XRRigidTransform position and orientation meet [SameObject] requirements"; + +// xrRigidTransform_inverse.https.html already checks [SameObject] requirement +// for XRRigidTransform.inverse. It's in a separate test because there are +// additional constraints around how the inverse attribute should work besides +// just [SameObject]. + +let testFunction = + (session, fakeDeviceController, t) => new Promise((resolve, reject) => { + + let transform = new XRRigidTransform( + { x: -9.0, y: 8.0, z: -7.0 }, + { x: 6.0, y: -5.0, z: 4.0, w: 3.0 }); + + let position = transform.position; + let orientation = transform.orientation; + let matrix = transform.matrix; + t.step(() => { + assert_equals(position, transform.position, + "XRRigidTransform.position returns the same object."); + assert_equals(orientation, transform.orientation, + "XRRigidTransform.orientation returns the same object."); + assert_equals(matrix, transform.matrix, + "XRRigidTransform.matrix returns the same object."); + }); + + resolve(); +}); + +xr_session_promise_test(testName, testFunction, TRACKED_IMMERSIVE_DEVICE, + 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html new file mode 100644 index 0000000000..2b3403fe46 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + let immersiveTestName = "XRSession requestAnimationFrame callbacks can be " + + "unregistered with cancelAnimationFrame for immersive sessions"; + let nonImmersiveTestName = "XRSession requestAnimationFrame callbacks can be " + + "unregistered with cancelAnimationFrame for non-immersive sessions"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = (session) => new Promise((resolve, reject) => { + + // Schedule and immediately cancel animation frame + session.cancelAnimationFrame(session.requestAnimationFrame( + (time, vrFrame) => { reject("Cancelled frame callback was called"); })); + + let counter = 0; + let handle; + function onFrame(time, vrFrame) { + // Cancel the second animation frame. + if (handle != 0) { + session.cancelAnimationFrame(handle); + handle = 0; + } + + // Run a few more animation frames to be sure that the cancelled frame isn't + // going to call. + counter++; + if (counter >= 4) { + // Ok, we're done here. + resolve(); + } else { + session.requestAnimationFrame(onFrame); + } + } + + // Schedule two animation frame and cancel one during on animation frame. + session.requestAnimationFrame(onFrame); + handle = session.requestAnimationFrame( + (time, vrFrame) => { reject("Cancelled frame callback was called"); }); + }); + + xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline'); + + </script> +</body> + diff --git a/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html new file mode 100644 index 0000000000..f036e3080f --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + let immersiveTestName = "XRSession cancelAnimationFrame does not have unexpected " + + "behavior when given invalid handles on immersive testSession"; + let nonImmersiveTestName = "XRSession cancelAnimationFrame does not have unexpected " + + "behavior when given invalid handles on non-immersive testSession"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = (testSession) => new Promise((resolve) => { + let counter = 0; + + function onFrame(time, vrFrame) { + if(counter <= 10) { + testSession.requestAnimationFrame(onFrame); + } else { + resolve(); + } + counter++; + } + + let handle = testSession.requestAnimationFrame(onFrame); + testSession.cancelAnimationFrame(0); + testSession.cancelAnimationFrame(-1); + testSession.cancelAnimationFrame(handle + 1); + testSession.cancelAnimationFrame(handle - 1); + testSession.cancelAnimationFrame(0.5); + testSession.cancelAnimationFrame(null); + }); + + xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_enabledFeatures.https.html b/testing/web-platform/tests/webxr/xrSession_enabledFeatures.https.html new file mode 100644 index 0000000000..ba9045597c --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_enabledFeatures.https.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<body> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<canvas></canvas> +<script> + + const testName = "Validate enabledFeatures on XRSession"; + + const supportedFeatureList = [ + 'viewer', + 'local', + 'local-floor', + 'anchors', + 'hit-test', + 'dom-overlay' + ]; + + const fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: supportedFeatureList + }; + + // NOTE: We explicit don't ask for the 'default' features of viewer/local to + // verify that they are being added here. + const requestFeatures = [ + 'local-floor', + 'anchors', + 'secondary-views', + 'camera-access', + ]; + +const testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve,reject) => { + const unsupportedRequestedFeatures = []; + + for (const feature of requestFeatures) { + if (!supportedFeatureList.includes(feature)) + unsupportedRequestedFeatures.push(feature); + } + + const enabledFeatures = session.enabledFeatures; + const modeDefaultFeatures = DEFAULT_FEATURES[session.mode]; + + t.step(() => { + // Whether they were requested or not, all Default features should be + // enabled. + for (const feature of modeDefaultFeatures) { + assert_true(enabledFeatures.includes(feature), + "Did not support default feature: " + feature); + } + + // Assert that we asked for everything that was included apart from the + // default features + for (const feature of enabledFeatures) { + assert_true(requestFeatures.includes(feature) || + modeDefaultFeatures.includes(feature), + "Enabled unrequested feature: " + feature); + } + + // Assert that all of the features we asked are either excluded because + // they were unsupported, or included because they were supported. + for (const feature of requestFeatures) { + if (unsupportedRequestedFeatures.includes(feature)) { + assert_false(enabledFeatures.includes(feature), + "Enabled supposedly unsupported feature: " + feature); + } else { + assert_true(enabledFeatures.includes(feature), + "Did not enable supposedly supported feature: " + feature); + } + } + }); + + resolve(); + }); +}; + +xr_session_promise_test(testName, testFunction, + fakeDeviceInitParams, 'immersive-vr', { optionalFeatures: requestFeatures }); + +</script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_end.https.html b/testing/web-platform/tests/webxr/xrSession_end.https.html new file mode 100644 index 0000000000..dccc550be5 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_end.https.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + const immersivetestName = "end event fires when immersive session ends"; + const nonimmersiveTestName = "end event fires when non-immersive session ends"; + let watcherDone = new Event("watcherdone"); + const fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = function(session, testDeviceController, t) { + let eventWatcher = new EventWatcher(t, session, ["end", "watcherdone"]); + let eventPromise = eventWatcher.wait_for(["end", "watcherdone"]); + + function onSessionEnd(event) { + t.step( () => { + assert_equals(event.session, session); + + let eventSession = event.session; + assert_equals(eventSession, event.session, + "XRSessionEvent.session returns the same object."); + + session.dispatchEvent(watcherDone); + }); + } + session.addEventListener("end", onSessionEnd, false); + session.end(); + + return eventPromise; + }; + + xr_session_promise_test(immersivetestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + xr_session_promise_test(nonimmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline'); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_features_deviceSupport.https.html b/testing/web-platform/tests/webxr/xrSession_features_deviceSupport.https.html new file mode 100644 index 0000000000..2e06523224 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_features_deviceSupport.https.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<body> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<canvas></canvas> +<script> + + let testName = + "Immersive XRSession requests with no supported device should reject"; + + let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VALID_VIEWS, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: [ + "viewer", + "local", + "local-floor"] + }; + + + xr_promise_test(testName, + (t) => { + function session_resolves(sessionMode, sessionInit) { + return navigator.xr.requestSession(sessionMode, sessionInit) + .then((session) => { + return session.end(); + }); + } + + return navigator.xr.test.simulateDeviceConnection(fakeDeviceInitParams) + .then((controller) => + promise_simulate_user_activation(() => { + // Attempting to request required features that aren't supported by + // the device should reject. + + return promise_rejects_dom(t, "NotSupportedError", + navigator.xr.requestSession("immersive-vr", { + requiredFeatures: ['bounded-floor'] + })) + }).then(() => promise_simulate_user_activation(() => { + // Attempting to request with an unsupported feature as optional + // should succeed + return session_resolves("immersive-vr", { + optionalFeatures: ['bounded-floor'] + }); + })).then(() => promise_simulate_user_activation(() => { + // Attempting to request with supported features only should succeed. + return session_resolves("immersive-vr", { + requiredFeatures: ['local', 'local-floor'] + }) + })) + ); + }); + +</script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_input_events_end.https.html b/testing/web-platform/tests/webxr/xrSession_input_events_end.https.html new file mode 100644 index 0000000000..b7f64617ee --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_input_events_end.https.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Calling end during an input callback stops processing at the right time"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let gl = null; + +function requestImmersiveSession() { + return new Promise((resolve, reject) => { + navigator.xr.test.simulateUserActivation(() => { + navigator.xr.requestSession('immersive-vr').then((session) => { + session.updateRenderState({ + baseLayer: new XRWebGLLayer(session, gl) + }); + + resolve(session); + }, (err) => { + reject(err); + }); + }); + }); +} + +let testFunction = function(session, fakeDeviceController, t, sessionObjects) { + gl = sessionObjects.gl; + // helper method to send a click and then request a dummy animation frame to + // ensure that the click propagates. We're doing everything in these tests + // from event watchers, we just need to trigger the add/click to make the + // event listeners callback. + function sendClick(session) { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + profiles: [], + selectionClicked: true + }); + requestSkipAnimationFrame(session, () => {}); + } + + function sessionEndTest(endEvent, eventOrder) { + return requestImmersiveSession().then((session) => { + let eventWatcher = new EventWatcher(t, session, + ["inputsourceschange", "selectstart", "select", "selectend", "end"]); + let eventPromise = eventWatcher.wait_for(eventOrder); + + session.addEventListener(endEvent, ()=> { + session.end(); + }, false); + + sendClick(session); + return eventPromise; + }); + } + + // End our first session, just for simplicity, then make end session calls + // during each of our input events, and ensure that events stop processing + // and no crashes occur from calling end inside the event callbacks. + return session.end().then(() => { + return sessionEndTest("inputsourceschange", ["inputsourceschange", "end"]); + }).then(() => { + return sessionEndTest("selectstart", ["inputsourceschange", "selectstart", "selectend", "end"]); + }).then(() => { + return sessionEndTest("select", ["inputsourceschange", "selectstart", "select", "selectend", "end"]); + }).then(() => { + return sessionEndTest("selectend", ["inputsourceschange", "selectstart", "select", "selectend", "end"]); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrSession_prevent_multiple_exclusive.https.html b/testing/web-platform/tests/webxr/xrSession_prevent_multiple_exclusive.https.html new file mode 100644 index 0000000000..602bbc4cb9 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_prevent_multiple_exclusive.https.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <canvas></canvas> + + <script> + xr_promise_test( + "Test prevention of multiple simultaneous immersive sessions", + (t) => { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then( (controller) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + resolve(navigator.xr.requestSession('immersive-vr') + .then( (session) => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + // Requesting a second immersive session when another immersive + // session is active should fail. Immersive sessions + // should take up the users entire view, and therefore it should + // be impossible for a user to be engaged with more than one. + resolve(promise_rejects_dom( + t, + "InvalidStateError", + navigator.xr.requestSession('immersive-vr') + ).then( () => { + // End the immersive session and try again. Now the immersive + // session creation should succeed. + return session.end().then( () => new Promise((resolve) => { + navigator.xr.test.simulateUserActivation( () => { + resolve(navigator.xr.requestSession('immersive-vr')); + }); + })); + })); + }); + }))); + }); + })); + }); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.https.html b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.https.html new file mode 100644 index 0000000000..ee29b8794a --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + let immersiveTestName = "XRSession requestAnimationFrame calls the " + + "provided callback for an immersive session"; + let nonImmersiveTestName = "XRSession requestAnimationFrame calls the " + + "provided callback a non-immersive session"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = (testSession) => new Promise((resolve) => { + function onFrame(time, xrFrame) { + assert_true(xrFrame instanceof XRFrame); + // Test does not complete until the returned promise resolves. + resolve(); + } + + testSession.requestAnimationFrame(onFrame); + }); + + xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html new file mode 100644 index 0000000000..812c79c1d5 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + const testName = "RequestAnimationFrame resolves with good data"; + + const fakeDeviceInitOptions = TRACKED_IMMERSIVE_DEVICE; + + let testSession; + + function checkView(view) { + assert_not_equals(view, null); + assert_not_equals(view.transform, null); + + let inv_view_transform = view.transform.inverse; + assert_not_equals(inv_view_transform, null); + assert_not_equals(inv_view_transform.matrix, null); + assert_equals(inv_view_transform.matrix.length, 16); + } + + let testFunction = function(session, testDeviceController) { + testSession = session; + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve) => { + + function onFrame(time, xrFrame) { + assert_true(xrFrame instanceof XRFrame); + + let viewerPose = xrFrame.getViewerPose(referenceSpace); + + assert_not_equals(viewerPose, null); + for(let i = 0; i < IDENTITY_MATRIX.length; i++) { + assert_equals(viewerPose.transform.matrix[i], IDENTITY_MATRIX[i]); + } + + assert_not_equals(viewerPose.views, null); + assert_equals(viewerPose.views.length, 2); + checkView(viewerPose.views[0]); + checkView(viewerPose.views[1]); + + // Test does not complete until the returned promise resolves. + resolve(); + } + + testSession.requestAnimationFrame(onFrame); + }) + ); + } + + xr_session_promise_test( + testName, testFunction, fakeDeviceInitOptions, 'immersive-vr'); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getViewerPose.https.html b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getViewerPose.https.html new file mode 100644 index 0000000000..68ad344320 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getViewerPose.https.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script src="resources/webxr_test_asserts.js"></script> + + <script> + + let immersiveTestName = + "XRFrame getViewerPose updates on the next frame for immersive sessions"; + let nonImmersiveTestName = + "XRFrame getViewerPose updates on the next frame for non-immersive sessions"; + + let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: VALID_VIEWS, + supportedFeatures: ALL_FEATURES + }; + + // A valid pose matrix/transform for when we don't care about specific values + // Note that these two should be identical, just different representations + const expectedPoseMatrix = [0, 1, 0, 0, + 0, 0, 1, 0, + 1, 0, 0, 0, + 1, 1, 1, 1]; + + const poseTransform = { + position: [1, 1, 1], + orientation: [0.5, 0.5, 0.5, 0.5] + }; + + let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve, reject) => { + let counter = 0; + function onFrame(time, vrFrame) { + if (counter == 0) { + t.step( () => { + // Expecting to not get a pose since none has been supplied + assert_equals(vrFrame.getViewerPose(referenceSpace), null); + + fakeDeviceController.setViewerOrigin(poseTransform); + + // Check that pose does not update pose within the same frame. + assert_equals(vrFrame.getViewerPose(referenceSpace), null); + }); + + // In order to avoid race conditions, after we've set the viewer + // pose, we queue up the next requestAnimationFrame. This should + // ensure that the next frame will be able to get the appropriate + // pose. + // Note that since the next frame will immediately resolve and end + // the test we only need to request a new frame once, here. + session.requestAnimationFrame(onFrame); + } else { + t.step( () => { + let pose = vrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + + let poseMatrix = pose.transform.matrix; + assert_not_equals(poseMatrix, null); + assert_matrix_approx_equals(poseMatrix, expectedPoseMatrix); + }); + + // Finished. + resolve(); + } + counter++; + } + + session.requestAnimationFrame(onFrame); + })); + }; + + xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline', { 'requiredFeatures': ['local'] }); + xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_timestamp.https.html b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_timestamp.https.html new file mode 100644 index 0000000000..ee6bb49432 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_timestamp.https.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +const TEN_SECONDS = 10000; // 10k ms in ten seconds +const ONE_MINUTE = 60000; // 60k ms in one minute + +let immersiveTestName = "XRFrame getViewerPose updates on the next frame for immersive"; +let nonImmersiveTestName = "XRFrame getViewerPose updates on the next frame for non-immersive"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer') + .then((referenceSpace) => new Promise((resolve, reject) => { + let counter = 0; + let windowFrameTime = 0; + let frameTime = 0; + let lastFrameTime = 0; + + let firstFrame = true; + + function onFrameFirst(time, xrFrame) { + lastFrameTime = frameTime; + frameTime = time; + let now = performance.now(); + + t.step( () => { + if(firstFrame) { + // This callback must be the first one called. + assert_equals(counter, 0); + } else { + // If it's a second animation frame, the timestamp must be greater + // than the timestamp on a previous frame. + assert_greater_than(frameTime, lastFrameTime); + // ... but not grater than 10 seconds. + assert_approx_equals(frameTime, lastFrameTime, TEN_SECONDS); + } + + // There's going to be some disparity between performance.now() and + // the timestamp passed into the callback, but it shouldn't be huge. + // If they're more than ten seconds apart something has gone horribly + // wrong. + assert_approx_equals(frameTime, now, TEN_SECONDS); + }); + + if (firstFrame) { + // We also want this method to run for the second animation frame. + session.requestAnimationFrame(onFrameFirst); + } else { + resolve(); + } + + firstFrame = false; + counter++; + } + + function onFrameSubsequent(time, xrFrame) { + t.step( () => { + // The timestamp passed to this callback should be exactly equal to + // the one passed to the first callback in this set. + assert_equals(time, frameTime); + }); + + counter++; + } + + function onFrameLast(time, xrFrame) { + t.step( () => { + // Make sure all the previous callbacks fired as expected. + assert_equals(counter, 11); + }); + } + + session.requestAnimationFrame(onFrameFirst); + // Queue up several callbacks + for (let i = 0; i < 10; ++i) { + session.requestAnimationFrame(onFrameSubsequent); + } + session.requestAnimationFrame(onFrameLast); + + })); +}; + +xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr'); +xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace.https.html b/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace.https.html new file mode 100644 index 0000000000..b8765fd060 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace.https.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + + let immersiveTestName = + "Immersive XRSession requestReferenceSpace returns expected objects"; + let nonImmersiveTestName = + "Non-immersive XRSession requestReferenceSpace returns expected objects"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = function(session, fakeDeviceController, t) { + return promise_rejects_js(t, TypeError, session.requestReferenceSpace('foo')) + .then(() => Promise.all([ + session.requestReferenceSpace('viewer').then( (referenceSpace) => { + t.step(() => { + assert_true(referenceSpace instanceof XRSpace, + "identity reference space is not correct type."); + assert_true(referenceSpace instanceof XRReferenceSpace, + "identity reference space is not correct type."); + }); + }), + session.requestReferenceSpace('local').then( (referenceSpace) => { + t.step(() => { + assert_true(referenceSpace instanceof XRSpace, + "eye-level stationary reference space is not correct type."); + assert_true(referenceSpace instanceof XRReferenceSpace, + "eye-level stationary reference space is not correct type."); + }); + }), + session.requestReferenceSpace('local-floor').then( (referenceSpace) => { + t.step(() => { + assert_true(referenceSpace instanceof XRSpace, + "floor-level stationary reference space is not correct type."); + assert_true(referenceSpace instanceof XRReferenceSpace, + "floor-level stationary reference space is not correct type."); + }); + }) + ])) + .then(() => { + if (session.mode == 'inline') { + // Bounded reference spaces are not allowed in inline sessions. + return promise_rejects_dom(t, "NotSupportedError", session.requestReferenceSpace('bounded-floor')) + } + }) + .then(() => { + if (session.mode == 'inline') { + // Unbounded reference spaces are not allowed in inline sessions. + return promise_rejects_dom(t, "NotSupportedError", session.requestReferenceSpace('unbounded')) + } + }) + }; + + // Reference spaces that aren't included in the default feature list must + // be specified as a required or optional features on session creation. + xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr', + {requiredFeatures: ['local-floor'], + optionalFeatures: ['bounded-floor', 'unbounded']}); + xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline', + {requiredFeatures: ['local'], + optionalFeatures: ['local-floor']}); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace_features.https.html b/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace_features.https.html new file mode 100644 index 0000000000..a20223fca3 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestReferenceSpace_features.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + <script> + + let makeSpaceTest = (space_type) => { + return (session, fakeDeviceController, t) => { + return session.requestReferenceSpace(space_type).then( (referenceSpace) => { + t.step(() => { + assert_true(referenceSpace instanceof XRReferenceSpace, + "reference space is not correct type."); + }); + }); + }; + }; + + let makeInvalidSpaceTest = (space_type) => { + return (session, fakeDeviceController, t) => { + return promise_rejects_dom(t, "NotSupportedError", + session.requestReferenceSpace(space_type), + "requestReferenceSpace('" + space_type + "')"); + }; + }; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + // Test that reference spaces matching default features work. + xr_session_promise_test( + "Non-immersive session supports viewer space by default", + makeSpaceTest('viewer'), fakeDeviceInitParams, 'inline', {}); + xr_session_promise_test( + "Immersive session supports viewer space by default", + makeSpaceTest('viewer'), fakeDeviceInitParams, 'immersive-vr', {}); + xr_session_promise_test( + "Immersive session supports local space by default", + makeSpaceTest('local'), fakeDeviceInitParams, 'immersive-vr', {}); + + // Test that session-appropriate non-default reference spaces work when requested + xr_session_promise_test( + "Non-immersive session supports local space when required", + makeSpaceTest('local'), fakeDeviceInitParams, 'inline', + {requiredFeatures: ['local']}); + xr_session_promise_test( + "Non-immersive session supports local space when optional", + makeSpaceTest('local'), fakeDeviceInitParams, 'inline', + {optionalFeatures: ['local']}); + xr_session_promise_test( + "Non-immersive session supports local-floor space when required", + makeSpaceTest('local-floor'), fakeDeviceInitParams, 'inline', + {requiredFeatures: ['local-floor']}); + xr_session_promise_test( + "Immersive session supports local-floor space when required", + makeSpaceTest('local-floor'), fakeDeviceInitParams, 'immersive-vr', + {requiredFeatures: ['local-floor']}); + xr_session_promise_test( + "Immersive session supports local-floor space when optional", + makeSpaceTest('local-floor'), fakeDeviceInitParams, 'immersive-vr', + {optionalFeatures: ['local-floor']}); + + // Test that inline space can't request 'bounded-floor' or 'unbounded' + xr_session_promise_test( + "Non-immersive session rejects bounded-floor space even when requested", + makeInvalidSpaceTest('bounded-floor'), fakeDeviceInitParams, 'inline', + {optionalFeatures: ['bounded-floor']}); + xr_session_promise_test( + "Non-immersive session rejects unbounded space even when requested", + makeInvalidSpaceTest('unbounded'), fakeDeviceInitParams, 'inline', + {optionalFeatures: ['unbounded']}); + + // Test that reference spaces that aren't default features are rejected + // when not requested as a feature. + xr_session_promise_test( + "Non-immersive session rejects local space if not requested", + makeInvalidSpaceTest('local'), fakeDeviceInitParams, 'inline', {}); + xr_session_promise_test( + "Immersive session rejects local-floor space if not requested", + makeInvalidSpaceTest('local-floor'), fakeDeviceInitParams, 'immersive-vr', {}); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_requestSessionDuringEnd.https.html b/testing/web-platform/tests/webxr/xrSession_requestSessionDuringEnd.https.html new file mode 100644 index 0000000000..38133b616a --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_requestSessionDuringEnd.https.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + function testFunctionGenerator(createSessionFromEventCallback) { + return function(session, testDeviceController, t) { + let done = false; + + function createSession() { + return new Promise((resolve) => { + navigator.xr.requestSession("immersive-vr") + .then((new_session) => { + // The test framework ensures that the created session ends, + // but will not do cleanup for this session, so if it gets + // created, we need to ensure that it gets cleaned up. + return new_session.end(); + }).then(() => { + done = true; + resolve(); + }).catch((err) => { + // Only one catch is needed for the whole promise chain. + // If ending the new session throws, it's fine to fail as + // we'd otherwise end up in a bad state. + t.step(() => { + assert_unreached("Session creation should not throw: " + err); + }); + }); + }); + } + + function onSessionEnd() { + if (createSessionFromEventCallback) { + createSession(); + } + } + + session.addEventListener("end", onSessionEnd, false); + + // We need to simulate the user activation before we call end as + // otherwise (depending on the implementation) it can interfere with + // the scheduling of the dispatched event/promise, and make session + // creation succeed even when there may be bugs preventing it from + // doing so in real scenarios. + navigator.xr.test.simulateUserActivation(() => { + session.end().then(() => { + if (!createSessionFromEventCallback) { + createSession(); + } + }); + }); + + return t.step_wait(() => done); + }; + } + + xr_session_promise_test("Create new session in OnSessionEnded event", + testFunctionGenerator(/*createSessionFromEventCallback=*/true), + TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); + + xr_session_promise_test("Create mew session in end promise", + testFunctionGenerator(/*createSessionFromEventCallback=*/false), + TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_sameObject.https.html b/testing/web-platform/tests/webxr/xrSession_sameObject.https.html new file mode 100644 index 0000000000..1795ee7a72 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_sameObject.https.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRSession attributes meet [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + let input_source = fakeDeviceController.simulateInputSourceConnection({ + handedness: "right", + targetRayMode: "tracked-pointer", + pointerOrigin: VALID_POINTER_TRANSFORM, + gripOrigin: VALID_GRIP_TRANSFORM, + profiles: ["foo", "bar"] + }); + + requestSkipAnimationFrame(session, (time, xrFrame) => { + let renderState = session.renderState; + let sources = session.inputSources; + + t.step(() => { + assert_not_equals(renderState, null, "renderState must not be null."); + assert_not_equals(sources, null, "inputSources must not be null."); + + // Make sure [SameObject] attributes actually have the same object + // returned each time they are accessed. + assert_equals(renderState, session.renderState, + "XRSession.renderState returns the same object."); + assert_equals(sources, session.inputSources, + "XRSession.inputSources returns the same object."); + }); + + session.requestAnimationFrame((time, xrFrame) => { + t.step(() => { + // Make sure the attributes still return the same object on the next + // frame. + assert_equals(renderState, session.renderState, + "XRSession.renderState returns the same object."); + assert_equals(sources, session.inputSources, + "XRSession.inputSources returns the same object."); + }); + + // Even though changing handedness on the input source should cause that + // source to be re-created, it should not cause the entire + // XRInputSourceArray object on XRSession to be re-created. + input_source.setHandedness("left"); + session.requestAnimationFrame((time, xrFrame) => { + t.step(() => { + assert_equals(renderState, session.renderState, + "XRSession.renderState returns the same object."); + assert_equals(sources, session.inputSources, + "XRSession.inputSources returns the same object."); + }); + resolve(); + }); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrSession_viewer_availability.https.html b/testing/web-platform/tests/webxr/xrSession_viewer_availability.https.html new file mode 100644 index 0000000000..b630be14ab --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_viewer_availability.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<body> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<canvas></canvas> +<script> + + let testName = + "Inline viewer support with no device"; + + // Purposefully not connecting a device to ensure that viewer is always + // supported if that is the only feature requested. + xr_promise_test(testName, + (t) => { + function session_resolves(sessionMode, sessionInit) { + return navigator.xr.requestSession(sessionMode, sessionInit) + .then(session => session.end()); + } + + function session_rejects(expected, sessionMode, sessionInit) { + return promise_rejects_dom(t, expected, navigator.xr.requestSession(sessionMode, sessionInit) + .then(session => session.end())); + } + + return session_resolves('inline', { + // RequestSession with 'viewer' as a required featre should succeed, even + // without user activation. + requiredFeatures: ['viewer'] + }) + .then(() => { + // RequestSession with 'viewer' as an optional feature should succeed, even + // without user activation. + return session_resolves('inline', { + optionalFeatures: ['viewer'] + }) + }) + .then(() => { + // RequestSession with no requirements should succeed. + return session_resolves('inline', {}); + }) + .then(() => { + // RequestSession with non-viewer optional features should fail + // without user activation. + return session_rejects("SecurityError", 'inline', { + optionalFeatures: ['local'] + }); + }) + .then(() => { + // RequestSession with non-viewer required features should fail + // without user activation. + return session_rejects("SecurityError", 'inline', { + optionalFeatures: ['local'] + }); + }) + .then(() => promise_simulate_user_activation(() => { + // RequestSession with unsupported optional features should succeed. + return session_resolves('inline', { + requiredFeatures: ['viewer'], + optionalFeatures: ['local'] + }) + })) + .then(() => promise_simulate_user_activation(() => { + // Request with unsupported required features should reject. + return session_rejects("NotSupportedError", 'inline', { + requiredFeatures: ['local'] + }); + })); + }); + +</script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_viewer_referenceSpace.https.html b/testing/web-platform/tests/webxr/xrSession_viewer_referenceSpace.https.html new file mode 100644 index 0000000000..1768c96849 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_viewer_referenceSpace.https.html @@ -0,0 +1,73 @@ +<!DOCTYPE html> +<body> + <script src=/resources/testharness.js></script> + <script src=/resources/testharnessreport.js></script> + <script src="resources/webxr_util.js"></script> + <script src="resources/webxr_test_constants.js"></script> + + <script> + + let immersiveTestName = + "Identity reference space provides correct poses for immersive sessions"; + let inlineTestName = + "Identity reference space provides correct poses for inline sessions"; + + let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + + let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer') + .then((referenceSpace) => new Promise((resolve, reject) => { + let counter = 0; + function onFrame(time, xrFrame) { + session.requestAnimationFrame(onFrame); + if (counter == 0) { + t.step( () => { + // Expect to always get a pose, even if none has been supplied. + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + + let poseMatrix = pose.transform.matrix; + assert_not_equals(poseMatrix, null); + + for(let i = 0; i < poseMatrix.length; i++) { + // "0 +" is to accept -0 which is equivalent to 0 in the + // matrix. + assert_equals(0 + poseMatrix[i], IDENTITY_MATRIX[i]); + } + + fakeDeviceController.setViewerOrigin(VALID_POSE_TRANSFORM); + }); + } else { + t.step( () => { + // Assert that the identity matrix is always given as the pose + // even when a valid pose is set by the device. + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + + let poseMatrix = pose.transform.matrix; + assert_not_equals(poseMatrix, null); + + for(let i = 0; i < poseMatrix.length; i++) { + // "0 +" is to accept -0 which is equivalent to 0 in the + // matrix. + assert_equals(0 + poseMatrix[i], IDENTITY_MATRIX[i]); + } + }); + + // Finished. + resolve(); + } + counter++; + } + + session.requestAnimationFrame(onFrame); + })); + }; + + xr_session_promise_test(inlineTestName, testFunction, + fakeDeviceInitParams, 'inline'); + xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); + + </script> +</body> diff --git a/testing/web-platform/tests/webxr/xrSession_visibilityState.https.html b/testing/web-platform/tests/webxr/xrSession_visibilityState.https.html new file mode 100644 index 0000000000..12655f4be0 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrSession_visibilityState.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "Ensures that the XRSession's visibilityState is correctly " + + "reported and that the associated visibilitychange event fires."; + +let watcherDone = new Event("watcherdone"); +let frameFired = new Event("framefired"); + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + let eventWatcher = new EventWatcher( + t, session, ["visibilitychange", "framefired", "watcherdone"]); + let eventPromise = eventWatcher.wait_for( + ["visibilitychange", "visibilitychange", "framefired", "watcherdone"]); + + function onFrame(timestamp, frame) { + t.step( () => { + // The session should not fire any animation frames while the visibility + // state is hidden. + assert_not_equals(session.visibilityState, "hidden"); + }); + + // Make sure the frame does fire when the visibility is changed back to "visible" + session.dispatchEvent(frameFired); + } + + function onSessionVisibilityChangeHidden(event) { + t.step( () => { + assert_equals(session.visibilityState, "hidden"); + }); + + session.removeEventListener("visibilitychange", onSessionVisibilityChangeHidden, false); + session.addEventListener("visibilitychange", onSessionVisibilityChangeVisible, false); + + session.requestAnimationFrame(onFrame) + + t.step_timeout(() => { + fakeDeviceController.simulateVisibilityChange("visible"); + }, 300); + } + + function onSessionVisibilityChangeVisible(event) { + t.step( () => { + assert_equals(session.visibilityState, "visible"); + }); + + session.removeEventListener("visibilitychange", onSessionVisibilityChangeVisible, false); + session.addEventListener("visibilitychange", onSessionVisibilityChangeInvalid, false); + fakeDeviceController.simulateVisibilityChange("visible"); + + t.step_timeout(() => { + session.dispatchEvent(watcherDone); + }, 300); + } + + function onSessionVisibilityChangeInvalid(event) { + t.step( () => { + assert_not_reached("Should not fire visibilitychange events for the same state"); + }); + } + + t.step( () => { + // Session visibility should start out as "visible" + assert_equals(session.visibilityState, "visible"); + }); + + session.addEventListener("visibilitychange", onSessionVisibilityChangeHidden, false); + fakeDeviceController.simulateVisibilityChange("hidden"); + + return eventPromise; +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrStationaryReferenceSpace_floorlevel_updates.https.html b/testing/web-platform/tests/webxr/xrStationaryReferenceSpace_floorlevel_updates.https.html new file mode 100644 index 0000000000..f1e907d6bb --- /dev/null +++ b/testing/web-platform/tests/webxr/xrStationaryReferenceSpace_floorlevel_updates.https.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> +let immersiveTestName = "'floor-level' XRStationaryReferenceSpace updates properly when " + + "the transform changes for immersive sessions"; +let nonImmersiveTestName = "'floor-level' XRStationaryReferenceSpace updates properly when " + + "the transform changes for non-immersive sessions"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + // Don't need to request a frame/allow the stage updates to propagate before + // requesting local-floor because if the stage transform is set it just won't + // be emulated on the first frame, and we wait a frame before checking. + return session.requestReferenceSpace('local-floor') + .then((referenceSpace) => new Promise((resolve, reject) => { + function onFirstFrame(time, xrFrame) { + // On the first frame where the pose has been initialized, the stage + // should be using an emulated frame of reference because it has no + // stageParameters yet. So the pose should be ~1.5 meters off the floor. + t.step( () => { + let pose = xrFrame.getViewerPose(referenceSpace); + + let poseMatrix = pose.transform.matrix; + assert_approx_equals(poseMatrix[12], 0.0, FLOAT_EPSILON); + assert_greater_than(poseMatrix[13], 1.0); + assert_approx_equals(poseMatrix[14], 0.0, FLOAT_EPSILON); + + fakeDeviceController.setFloorOrigin(VALID_FLOOR_ORIGIN); + + // Need to request one animation frame for the new stage transform to + // propagate before we check that it's what we expect. + requestSkipAnimationFrame(session, onFrame); + }); + } + + function onFrame(time, xrFrame) { + t.step( () => { + // Check that stage transform was updated. + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + + let poseMatrix = pose.transform.matrix; + assert_matrix_approx_equals(poseMatrix, VALID_FLOOR_ORIGIN_MATRIX); + }); + + // Finished. + resolve(); + } + + // Need to wait one frame for the removal to propagate before we check that + // everything is at the expected emulated position. + requestSkipAnimationFrame(session, onFirstFrame); + })); +}; + +xr_session_promise_test(immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr', { 'requiredFeatures': ['local-floor'] }); +xr_session_promise_test(nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline', { 'requiredFeatures': ['local-floor'] }); + +</script> diff --git a/testing/web-platform/tests/webxr/xrView_eyes.https.html b/testing/web-platform/tests/webxr/xrView_eyes.https.html new file mode 100644 index 0000000000..ac017cd800 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrView_eyes.https.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let immersiveTestName = "XRView.eye is correct for immersive sessions"; +let nonImmersiveTestName = "XRView.eye is correct for non-immersive sessions"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer') + .then((space) => new Promise((resolve) => { + function onFrame(time, xrFrame) { + let viewer_pose = xrFrame.getViewerPose(space); + + if (session.mode == 'inline') { + // An inline session should report a single view with an eye type "none". + assert_equals(viewer_pose.views.length, 1); + assert_equals(viewer_pose.views[0].eye, "none"); + } else { + // An immersive session should report a two views with a left and right eye. + assert_equals(viewer_pose.views.length, 2); + assert_equals(viewer_pose.views[0].eye, "left"); + assert_equals(viewer_pose.views[1].eye, "right"); + } + + // Finished test. + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); +}; + +xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); +xr_session_promise_test(nonImmersiveTestName, testFunction, + fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrView_match.https.html b/testing/web-platform/tests/webxr/xrView_match.https.html new file mode 100644 index 0000000000..2ef430734d --- /dev/null +++ b/testing/web-platform/tests/webxr/xrView_match.https.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let testName = "XRFrame contains the expected views"; + +const fakeViews = [{ + eye:"left", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: LEFT_OFFSET, + resolution: VALID_RESOLUTION + }, { + eye:"right", + projectionMatrix: VALID_PROJECTION_MATRIX, + viewOffset: RIGHT_OFFSET, + resolution: VALID_RESOLUTION + }, +]; + +let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: fakeViews, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES +}; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer').then(function(viewerSpace) { + return session.requestReferenceSpace('local').then((referenceSpace) => new Promise((resolve) => { + function onFrame(time, xrFrame) { + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + assert_not_equals(pose.views, null); + assert_equals(pose.views.length, 2); + + { + let pose2 = xrFrame.getPose(viewerSpace, referenceSpace); + assert_not_equals(pose2, null); + + // This pose should have the same transform as the viewer pose, but without + // the views array. It should be an XRPose instead of the XRViewerPose derived + // class. + assert_true(pose2 instanceof XRPose); + assert_false(pose2 instanceof XRViewerPose); + assert_not_equals(pose2.transform, null); + assert_not_equals(pose2.transform.matrix, null); + assert_matrix_approx_equals(pose.transform.matrix, pose2.transform.matrix); + } + + // Ensure that two views are provided. + let leftView = pose.views[0]; + let rightView = pose.views[1]; + + // Ensure that the views are the right type. + assert_true(leftView instanceof XRView); + assert_true(rightView instanceof XRView); + + // Ensure that they have the expected eye enums. + assert_equals(leftView.eye, "left"); + assert_equals(rightView.eye, "right"); + + // Ensure they have the expected projection matrices. + assert_not_equals(leftView.projectionMatrix, null); + assert_not_equals(rightView.projectionMatrix, null); + + assert_matrix_approx_equals(leftView.projectionMatrix, VALID_PROJECTION_MATRIX); + assert_matrix_approx_equals(rightView.projectionMatrix, VALID_PROJECTION_MATRIX); + + // Finished test. + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); + }); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrView_oneframeupdate.https.html b/testing/web-platform/tests/webxr/xrView_oneframeupdate.https.html new file mode 100644 index 0000000000..6c50f6f0c9 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrView_oneframeupdate.https.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let testName = "XRView projection matrices update near and far depths on the " + + "next frame"; + +const fakeViews = [{ + eye:"left", + viewOffset: LEFT_OFFSET, + resolution: VALID_RESOLUTION, + fieldOfView: VALID_FIELD_OF_VIEW, + // The webxr-test-api requires that we still set this for now, but it is + // supposed to be ignored. + projectionMatrix: IDENTITY_MATRIX + }, { + eye:"right", + viewOffset: RIGHT_OFFSET, + resolution: VALID_RESOLUTION, + fieldOfView: VALID_FIELD_OF_VIEW, + // The webxr-test-api requires that we still set this for now, but it is + // supposed to be ignored. + projectionMatrix: IDENTITY_MATRIX + }, +]; + +let fakeDeviceInitParams = { + supportsImmersive: true, + supportedModes: ["inline", "immersive-vr"], + views: fakeViews, + viewerOrigin: IDENTITY_TRANSFORM, + supportedFeatures: ALL_FEATURES +}; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve) =>{ + let counter = 0; + + function onFrame(time, xrFrame) { + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + assert_not_equals(pose.views, null); + assert_equals(pose.views.length, 2); + if (counter == 0) { + session.requestAnimationFrame(onFrame); + + assert_matrix_approx_equals(pose.views[0].projectionMatrix, VALID_PROJECTION_MATRIX); + assert_matrix_approx_equals(pose.views[1].projectionMatrix, VALID_PROJECTION_MATRIX); + + // Update the near and far depths for the session. + session.updateRenderState({ + depthNear: 1.0, + depthFar: 10.0 }); + + // The projection matrices the views report should not take into + // account the new session depth values this frame. + assert_matrix_approx_equals(pose.views[0].projectionMatrix, VALID_PROJECTION_MATRIX); + assert_matrix_approx_equals(pose.views[1].projectionMatrix, VALID_PROJECTION_MATRIX); + } else { + // New depth values should be retained between frames. + assert_equals(session.renderState.depthNear, 1.0); + assert_equals(session.renderState.depthFar, 10.0); + + // Projection matricies should now reflect the new depth values, i.e. + // have changed. + assert_matrix_significantly_not_equals(pose.views[0].projectionMatrix, VALID_PROJECTION_MATRIX); + assert_matrix_significantly_not_equals(pose.views[1].projectionMatrix, VALID_PROJECTION_MATRIX); + resolve(); + } + counter++; + } + + session.requestAnimationFrame(t.step_func(onFrame)); + })); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrView_sameObject.https.html b/testing/web-platform/tests/webxr/xrView_sameObject.https.html new file mode 100644 index 0000000000..2defa72206 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrView_sameObject.https.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRView attributes meet [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + session.requestReferenceSpace('local').then((referenceSpace) => { + session.requestAnimationFrame((time, xrFrame) => { + // Make sure that the projectionMatrix and transform attributes on + // XRView always return the same object. + let viewerPose = xrFrame.getViewerPose(referenceSpace); + let view = viewerPose.views[0]; + + let transform = view.transform; + let projectionMatrix = view.projectionMatrix; + + t.step(() => { + assert_equals(transform, view.transform, + "XRView.transform returns the same object."); + assert_equals(projectionMatrix, view.projectionMatrix, + "XRView.projectionMatrix returns the same object."); + }); + + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrViewerPose_secondaryViews.https.html b/testing/web-platform/tests/webxr/xrViewerPose_secondaryViews.https.html new file mode 100644 index 0000000000..446fedf14f --- /dev/null +++ b/testing/web-platform/tests/webxr/xrViewerPose_secondaryViews.https.html @@ -0,0 +1,94 @@ +<!DOCTYPE html> +<body> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let nonImmersiveNoSecondaryTestName = + "Only primary views are returned if secondary views are not requested for non-immersive"; +let immersiveNoSecondaryTestName = + "Only primary views are returned if secondary views are not requested for immersive"; +let nonImmersiveSecondaryTestName = + "Requesting secondary views only returns primary views for non-immersive"; +let immersiveSecondaryTestName = + "Requesting secondary views returns both primary and secondary views for immersive"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let verifyView = function(view, eye, isFirstPersonObserver) { + assert_equals(view.eye, eye); + assert_equals(view.isFirstPersonObserver, isFirstPersonObserver); +}; + +let verifyInlineViews = function(views) { + // Inline sessions should never have secondary views regardless of whether + // it was requested or not. + assert_equals(views.length, 1); + verifyView(views[0], "none", false); +}; + +let verifyImmersiveViews = function(views, secondaryViewsEnabled) { + verifyView(views[0], "left", false); + verifyView(views[1], "right", false); + + if (secondaryViewsEnabled) { + assert_equals(views.length, 3); + verifyView(views[2], "none", true); + } else { + assert_equals(views.length, 2); + } +}; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve, reject) => { + function onFrame(time, xrFrame) { + t.step(() => { + let pose = xrFrame.getViewerPose(referenceSpace); + if (!pose) { + // For inline sessions, the window may have triggered this frame, + // instead of the session. This frame has no frame data which + // results in a null pose. + assert_true(session.mode == 'inline'); + session.requestAnimationFrame(onFrame); + return; + } + + if (session.mode == 'inline') { + verifyInlineViews(pose.views); + } else { + verifyImmersiveViews( + pose.views, + session.sessionInit['optionalFeatures'].includes('secondary-views')); + } + + resolve(); + }); + } + session.requestAnimationFrame(onFrame); + })); +}; + +xr_session_promise_test( + nonImmersiveNoSecondaryTestName, testFunction, fakeDeviceInitParams, 'inline', + {'requiredFeatures': ['local'], + 'optionalFeatures': []}); +xr_session_promise_test( + immersiveNoSecondaryTestName, testFunction, fakeDeviceInitParams, 'immersive-vr', + {'requiredFeatures': ['local'], + 'optionalFeatures': []}); +xr_session_promise_test( + nonImmersiveSecondaryTestName, testFunction, fakeDeviceInitParams, 'inline', + {'requiredFeatures': ['local'], + 'optionalFeatures': ['secondary-views']}); +xr_session_promise_test( + immersiveSecondaryTestName, testFunction, fakeDeviceInitParams, 'immersive-vr', + {'requiredFeatures': ['local'], + 'optionalFeatures': ['secondary-views']}); + +</script> +</body> diff --git a/testing/web-platform/tests/webxr/xrViewerPose_views_sameObject.https.html b/testing/web-platform/tests/webxr/xrViewerPose_views_sameObject.https.html new file mode 100644 index 0000000000..af64111d82 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrViewerPose_views_sameObject.https.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRViewerPose.views meets [SameObject] requirement"; + +let testFunction = function(session, fakeDeviceController, t) { + return new Promise((resolve) => { + session.requestReferenceSpace('local').then((referenceSpace) => { + session.requestAnimationFrame((time, xrFrame) => { + // Make sure that the views attribute is the same object each time we + // access it. This verifies that XRViewerPose does *not* do something + // spec-noncompliant such as creating and returning a new XRView array + // each time the attribute is accessed. + let viewerPose = xrFrame.getViewerPose(referenceSpace); + let views = viewerPose.views; + t.step(() => { + assert_equals(viewerPose.views, views, + "XRViewerPose.views returns the same object."); + }); + + resolve(); + }); + }); + }); +}; + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); +</script> diff --git a/testing/web-platform/tests/webxr/xrViewport_valid.https.html b/testing/web-platform/tests/webxr/xrViewport_valid.https.html new file mode 100644 index 0000000000..c64c56cfdd --- /dev/null +++ b/testing/web-platform/tests/webxr/xrViewport_valid.https.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let testName = "XRViewport attributes are valid"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t, sessionObjects) { + return session.requestReferenceSpace('local') + .then((referenceSpace) => new Promise((resolve) =>{ + let webglLayer = sessionObjects.glLayer; + function onFrame(time, xrFrame) { + t.step(() => { + let pose = xrFrame.getViewerPose(referenceSpace); + assert_not_equals(pose, null); + assert_not_equals(pose.views, null); + + if (session.sessionInit['optionalFeatures'].includes('secondary-views')) { + assert_equals(pose.views.length, 3); + } else { + assert_equals(pose.views.length, 2); + } + + // Ensure the views report the expected viewports into the WebGL layer. + for (let i = 0; i < pose.views.length; i++) { + let view = pose.views[i]; + let viewport = webglLayer.getViewport(view); + + assert_not_equals(viewport, null); + assert_true(viewport instanceof XRViewport); + + // Exact viewport values don't matter, but they must pass several tests: + + // Viewports have non-zero widths and heights. + assert_greater_than(viewport.width, 0); + assert_greater_than(viewport.height, 0); + + // Viewports are located within the framebuffer. + assert_greater_than_equal(viewport.x, 0); + assert_greater_than_equal(viewport.y, 0); + + assert_less_than_equal( + viewport.x + viewport.width, webglLayer.framebufferWidth); + assert_less_than_equal( + viewport.y + viewport.height, webglLayer.framebufferHeight); + + // Assume that the viewports are ordered from left to right. This is + // not a requirement by the WebXR spec, but is a sanity check since + // this is how Blink orders them. + if (i != 0) { + let previousView = pose.views[i - 1]; + let previousViewport = webglLayer.getViewport(previousView); + assert_less_than_equal(previousViewport.x + previousViewport.width, viewport.x); + } + } + }); + + resolve(); + } + session.requestAnimationFrame(onFrame); + })); +}; + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr', + {'optionalFeatures': []}); + +xr_session_promise_test( + testName + ' with secondary views requested', testFunction, fakeDeviceInitParams, 'immersive-vr', + {'optionalFeatures': ['secondary-views']}); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_constructor.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_constructor.https.html new file mode 100644 index 0000000000..5796e1e0ef --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_constructor.https.html @@ -0,0 +1,82 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +function testConstructor(t, gl) { + return navigator.xr.test.simulateDeviceConnection(TRACKED_IMMERSIVE_DEVICE) + .then(() => { + return navigator.xr.requestSession('inline') + .then((session) => { + try { + let webglLayerIncompatible = new XRWebGLLayer(session, gl); + } catch (err) { + assert_unreached("Inline XRWebGLLayers should not fail when created with a context that is not XRCompatible"); + } + }); + }) + .then(() => { + return new Promise((resolve) => { + navigator.xr.test.simulateUserActivation(() => { + let xrSession = null; + navigator.xr.requestSession('immersive-vr') + .then((session) => { + xrSession = session; + t.step_func(() => { + try { + let webglLayerIncompatible = new XRWebGLLayer(xrSession, gl); + assert_unreached("XRWebGLLayer should fail when created with a context that is not XRCompatible") + } catch (err) { + assert_equals(err.name, "InvalidStateError"); + } + }) + + return gl.makeXRCompatible(); + }).then(() => { + try { + let webglLayerGood = new XRWebGLLayer(xrSession, gl); + } catch (err) { + reject("XRWebGLLayer should not fail with valid arguments"); + } + + let lose_context_ext = gl.getExtension('WEBGL_lose_context'); + + gl.canvas.addEventListener('webglcontextlost', (ev) => { + ev.preventDefault(); + + try { + let webglLayerBadContext = new XRWebGLLayer(xrSession, gl); + reject("XRWebGLLayer should fail when created with a lost context"); + } catch (err) { + assert_equals(err.name, 'InvalidStateError'); + t.step_timeout(() => { lose_context_ext.restoreContext(); }, 100); + } + }); + + gl.canvas.addEventListener('webglcontextrestored', (ev) => { + resolve(xrSession.end().then(() => { + try { + let webglLayerBadSession = new XRWebGLLayer(xrSession, gl); + assert_unreached("XRWebGLLayer should fail when created with an ended session"); + } catch (err) { + assert_equals(err.name, 'InvalidStateError'); + } + })); + }); + + lose_context_ext.loseContext(); + }); + }); + }); + }); +} +xr_promise_test("Ensure that XRWebGLLayer's constructor throws appropriate errors using webgl", + testConstructor, null, 'webgl'); + +xr_promise_test("Ensure that XRWebGLLayer's constructor throws appropriate errors using webgl2", + testConstructor, null, 'webgl2'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_draw.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_draw.https.html new file mode 100644 index 0000000000..eb797cf93a --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_draw.https.html @@ -0,0 +1,92 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +let testName = + "Ensure a WebGL layer's framebuffer can only be drawn to inside a XR frame"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +// Very simple program setup with no error checking. +function setupProgram(gl, vertexSrc, fragmentSrc) { + let program = gl.createProgram(); + + let vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSrc); + gl.compileShader(vertexShader); + gl.attachShader(program, vertexShader); + + let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSrc); + gl.compileShader(fragmentShader); + gl.attachShader(program, fragmentShader); + + gl.linkProgram(program); + gl.useProgram(program); + + return program; +} + +let testFunction = + (session, fakeDeviceController, t, sessionObjects) => new Promise((resolve, reject) => { + let gl = sessionObjects.gl; + let webglLayer = sessionObjects.glLayer; + // Setup simple WebGL geometry to draw with. + let program = setupProgram(gl, + "attribute vec4 vPosition; void main() { gl_Position = vPosition; }", + "void main() { gl_FragColor = vec4(1.0,0.0,0.0,1.0); }" + ); + + let vertexObject = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexObject); + gl.bufferData( + gl.ARRAY_BUFFER, + new Float32Array([ 0,0.5,0, -0.5,-0.5,0, 0.5,-0.5,0 ]), + gl.STATIC_DRAW); + gl.enableVertexAttribArray(0); + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); + + let indexObject = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexObject); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2]), gl.STATIC_DRAW); + + let xrFramebuffer = webglLayer.framebuffer; + + function runDrawTests(expectedError) { + // Make sure we're starting with a clean error slate. + assert_equals(gl.getError(), gl.NO_ERROR); + + gl.bindFramebuffer(gl.FRAMEBUFFER, xrFramebuffer); + assert_equals(gl.getError(), gl.NO_ERROR); + + gl.clear(gl.COLOR_BUFFER_BIT); + assert_equals(gl.getError(), gl[expectedError]); + + gl.clear(gl.DEPTH_BUFFER_BIT); + assert_equals(gl.getError(), gl[expectedError]); + + gl.drawArrays(gl.TRIANGLES, 0, 3); + assert_equals(gl.getError(), gl[expectedError]); + + gl.drawElements(gl.TRIANGLES, 3, gl.UNSIGNED_BYTE, 0); + assert_equals(gl.getError(), gl[expectedError]); + } + + // Drawing operations outside of a XR frame should fail. + runDrawTests("INVALID_FRAMEBUFFER_OPERATION"); + + // Drawing operations within a XR frame should succeed. + session.requestAnimationFrame((time, xrFrame) => { + runDrawTests("NO_ERROR"); + resolve(); + }); +}); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_sameObject.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_sameObject.https.html new file mode 100644 index 0000000000..75efc626d6 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_sameObject.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +let testName = "XRWebGLLayer.framebuffer meets [SameObject] requirement"; + +let testFunction = + (session, fakeDeviceController, t, sessionObjects) => new Promise((resolve, reject) => { + let layer = new XRWebGLLayer(session, sessionObjects.gl, {}); + let framebuffer = layer.framebuffer; + t.step(() => { + assert_equals(framebuffer, layer.framebuffer, + "XRWebGLLayer.framebuffer returns the same object."); + }); + resolve(); +}); + +xr_session_promise_test( + testName, testFunction, TRACKED_IMMERSIVE_DEVICE, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_scale.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_scale.https.html new file mode 100644 index 0000000000..065fc3df4e --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_scale.https.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> + +let testName = "Ensure framebuffer scaling works as expected."; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t, sessionObjects) => new Promise((resolve, reject) => { + let gl = sessionObjects.gl; + let webglLayer = sessionObjects.glLayer; + let defaultFramebufferWidth = webglLayer.framebufferWidth; + let defaultFramebufferHeight = webglLayer.framebufferHeight; + let nativeScale = XRWebGLLayer.getNativeFramebufferScaleFactor(session); + + t.step(() => { + // Ensure the framebuffer default sizes and native scale are all non-zero. + assert_greater_than(defaultFramebufferWidth, 0); + assert_greater_than(defaultFramebufferHeight, 0); + assert_greater_than(nativeScale, 0); + + // The native scale should be the inverse for the default framebuffer scale. + assert_approx_equals(nativeScale, 1/fakeDeviceController.defaultFramebufferScale_, FLOAT_EPSILON); + }); + + webglLayer = new XRWebGLLayer(session, gl, { framebufferScaleFactor: nativeScale }); + t.step(() => { + // Ensure that requesting a native scale framebuffer gives the expected result. + assert_approx_equals(webglLayer.framebufferWidth, defaultFramebufferWidth*nativeScale, 2); + assert_approx_equals(webglLayer.framebufferHeight, defaultFramebufferHeight*nativeScale, 2); + }); + + webglLayer = new XRWebGLLayer(session, gl, { framebufferScaleFactor: 0 }); + t.step(() => { + // Ensure that the framebuffer has a lower bounds clamp. + assert_greater_than(webglLayer.framebufferWidth, 0); + assert_greater_than(webglLayer.framebufferHeight, 0); + }); + + webglLayer = new XRWebGLLayer(session, gl, { framebufferScaleFactor: 100 }); + t.step(() => { + // Ensure that the framebuffer has a reasonable upper bounds clamp. + assert_less_than(webglLayer.framebufferWidth, defaultFramebufferWidth*100); + assert_less_than(webglLayer.framebufferHeight, defaultFramebufferHeight*100); + }); + + resolve(); +}); + +xr_session_promise_test( + testName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer.https.html new file mode 100644 index 0000000000..f0d63de501 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer.https.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let immersiveTestName = "Ensure that the framebuffer given by the WebGL layer" + + " is opaque for immersive"; +let nonImmersiveTestName = "Ensure that the framebuffer given by the WebGL layer" + + " is opaque for non-immersive"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = + (session, fakeDeviceController, t, sessionObjects) => new Promise((resolve, reject) => { + let gl = sessionObjects.gl; + let webglLayer = sessionObjects.glLayer; + let xrFramebuffer = webglLayer.framebuffer; + + // Make sure we're starting with a clean error slate. + assert_equals(gl.getError(), gl.NO_ERROR); + + if (session.mode == 'inline') { + // Creating a layer with an inline session should return a framebuffer of + // null, and as such most of these tests won't apply. + assert_equals(xrFramebuffer, null); + resolve(); + return; + } + + assert_not_equals(xrFramebuffer, null); + + // The XR framebuffer is not bound to the GL context by default. + assert_not_equals(xrFramebuffer, gl.getParameter(gl.FRAMEBUFFER_BINDING)); + + assert_greater_than(webglLayer.framebufferWidth, 0); + assert_greater_than(webglLayer.framebufferHeight, 0); + + gl.bindFramebuffer(gl.FRAMEBUFFER, xrFramebuffer); + assert_equals(gl.getError(), gl.NO_ERROR); + + gl.deleteFramebuffer(xrFramebuffer); + assert_equals(gl.getError(), gl.INVALID_OPERATION); + + // Make sure the framebuffer is still bound after failed attempt to delete. + let boundFramebuffer = gl.getParameter(gl.FRAMEBUFFER_BINDING); + assert_equals(xrFramebuffer, boundFramebuffer); + assert_equals(gl.getError(), gl.NO_ERROR); + + // WebGL 2 does not throw an error and instead returns 0 when there are no + // attachments + if (gl.getParameter(gl.VERSION).includes("WebGL 1.0")) { + // Ensure the framebuffer attachment properties cannot be inspected. + let attachments = [ + gl.COLOR_ATTACHMENT0, + gl.DEPTH_ATTACHMENT, + gl.STENCIL_ATTACHMENT, + ]; + + let parameters = [ + gl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, + gl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, + gl.FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL, + gl.FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE, + ]; + + for (let attachment of attachments) { + for (let parameter of parameters) { + let value = gl.getFramebufferAttachmentParameter(gl.FRAMEBUFFER, attachment, parameter); + assert_equals(value, null); + assert_equals(gl.getError(), gl.INVALID_OPERATION); + } + } + } + + let width = 64; + let height = 64; + + // Ensure the framebuffer texture 2D attachmentments cannot be changed. + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + assert_equals(gl.getError(), gl.INVALID_OPERATION); + + // Ensure the framebuffer renderbuffer attachmentments cannot be changed. + let renderbuffer = gl.createRenderbuffer(); + gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer); + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height); + gl.framebufferRenderbuffer( + gl.FRAMEBUFFER, + gl.DEPTH_ATTACHMENT, + gl.RENDERBUFFER, + renderbuffer); + assert_equals(gl.getError(), gl.INVALID_OPERATION); + + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height); + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, renderbuffer); + assert_equals(gl.getError(), gl.INVALID_OPERATION); + + // Framebuffer status must be unsupported outside of a XR frame callback. + assert_equals(gl.checkFramebufferStatus(gl.FRAMEBUFFER), gl.FRAMEBUFFER_UNSUPPORTED); + + session.requestAnimationFrame((time, xrFrame) => { + // Framebuffer status must be complete inside of a XR frame callback. + assert_equals(gl.checkFramebufferStatus(gl.FRAMEBUFFER), gl.FRAMEBUFFER_COMPLETE); + // Finished. + resolve(); + }); +}); + +xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr'); + +xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline'); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer_stencil.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer_stencil.https.html new file mode 100644 index 0000000000..17f991f180 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer_stencil.https.html @@ -0,0 +1,223 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_constants.js"></script> + +<script> +let immersiveTestName = "Ensure that the framebuffer given by the WebGL layer" + + " works with stencil for immersive"; +let nonImmersiveTestName = "Ensure that the framebuffer given by the WebGL layer" + + " works with stencil for non-immersive"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +function createShader(gl, type, source) { + var shader = gl.createShader(type); + gl.shaderSource(shader, source); + gl.compileShader(shader); + var success = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (success) { + return shader; + } + + gl.deleteShader(shader); +} + +function createProgram(gl, vertexShader, fragmentShader) { + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + var success = gl.getProgramParameter(program, gl.LINK_STATUS); + if (success) { + return program; + } + + gl.deleteProgram(program); +} + +let testFunction = + (session, fakeDeviceController, t, sessionObjects) => new Promise((resolve, reject) => { + const gl = sessionObjects.gl; + const webglLayer = sessionObjects.glLayer; + const xrFramebuffer = webglLayer.framebuffer; + const webglCanvas = sessionObjects.gl.canvas; + + session.requestAnimationFrame((time, xrFrame) => { + t.step(() => { + // Make sure we're starting with a clean error slate. + gl.getError(); + assert_equals(gl.getError(), gl.NO_ERROR, "Should not initially have any errors"); + + if (session.mode === 'inline') { + // Creating a layer with an inline session should return a framebuffer of + // null, and as such most of these tests won't apply. + assert_equals(xrFramebuffer, null, 'inline, fbo = null'); + // We need to set canvas size here for inline session testing, since + // xrFramebuffer is null. + webglCanvas.width = webglCanvas.height = 300; + gl.bindFramebuffer(gl.FRAMEBUFFER, xrFramebuffer); + assert_equals(gl.getError(), gl.NO_ERROR, "Binding default framebuffer for inline session"); + } else { + assert_not_equals(xrFramebuffer, null, 'immersive, fbo != null'); + gl.bindFramebuffer(gl.FRAMEBUFFER, xrFramebuffer); + assert_equals(gl.getError(), gl.NO_ERROR, "Binding WebGLLayer framebuffer"); + } + + // Framebuffer status must be complete inside of a XR frame callback. + assert_equals(gl.checkFramebufferStatus(gl.FRAMEBUFFER), gl.FRAMEBUFFER_COMPLETE, "FBO complete"); + }); + gl.clearColor(1, 1, 1, 1); + + const vs = ` + attribute vec4 position; + uniform mat4 matrix; + void main() { + gl_Position = matrix * position; + } + `; + + const fs = ` + precision mediump float; + uniform vec4 color; + void main() { + gl_FragColor = color; + } + `; + const vertexShader = createShader(gl, gl.VERTEX_SHADER, vs); + const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fs); + const program = createProgram(gl, vertexShader, fragmentShader); + + const posLoc = gl.getAttribLocation(program, 'position'); + const matLoc = gl.getUniformLocation(program, 'matrix'); + const colorLoc = gl.getUniformLocation(program, 'color'); + + const buf = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, buf); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ + 0, -1, + 1, 1, + -1, 1, + ]), gl.STATIC_DRAW); + + gl.enableVertexAttribArray(posLoc); + gl.vertexAttribPointer( + posLoc, // attribute location + 2, // 2 value per vertex + gl.FLOAT, // 32bit floating point values + false, // don't normalize + 0, // stride (0 = base on type and size) + 0, // offset into buffer + ); + + let xrViewport; + if (session.mode == 'inline') { + xrViewport = { x: 0, y: 0, width: webglCanvas.width, height: webglCanvas.height }; + } else { + xrViewport = { x: 0, y: 0, width: webglLayer.framebufferWidth, height: webglLayer.framebufferHeight }; + } + + gl.viewport(xrViewport.x, xrViewport.y, xrViewport.width, xrViewport.height); + gl.scissor(xrViewport.x, xrViewport.y, xrViewport.width, xrViewport.height); + + // clear the stencil to 0 (the default) + gl.stencilMask(0xFF); + gl.clearStencil(0x0); + gl.disable( gl.SCISSOR_TEST ); + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); + + gl.useProgram(program); + let m4 = [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1]; + + // turn on the stencil + gl.enable(gl.STENCIL_TEST); + + // Drawing into a stencil, always passes, ref val 1, mask 0xFF + gl.stencilFunc( + gl.ALWAYS, + 1, + 0xFF, + ); + // Set it to replace with the ref val (which is 1) + gl.stencilOp( + gl.KEEP, // stencil test fails + gl.KEEP, // depth test fails + gl.REPLACE, // both tests pass + ); + + m4[0] = 0.2; m4[5] = 0.2; // scale x and y + // draw a white triangle + gl.uniform4fv(colorLoc, [1, 1, 1, 1]); // white + gl.uniformMatrix4fv(matLoc, false, m4); + gl.colorMask(false, false, false, false); + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.colorMask(true, true, true, true); + + // Stencil must be 0 + gl.stencilFunc( + gl.EQUAL, + 0, + 0xFF, + ); + // keep stencil unmodified during the draw pass + gl.stencilOp( + gl.KEEP, // stencil test fails + gl.KEEP, // depth test fails + gl.KEEP, // both tests pass + ); + + m4[0] = 0.9; m4[5] = -0.9; // scale x and y + // draw a large green triangle + gl.uniform4fv(colorLoc, [0, 1, 0, 1]); // green + gl.uniformMatrix4fv(matLoc, false, m4); + gl.drawArrays(gl.TRIANGLES, 0, 3); + + gl.flush(); + gl.finish(); + let pixels = new Uint8Array(4); + + // check that the main color is used correctly (green) + pixels[0] = pixels[1] = pixels[2] = pixels[3] = 30; + gl.readPixels(xrViewport.x + xrViewport.width / 2, xrViewport.y + xrViewport.height/4, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + if (pixels[0] == 0x0 && pixels[1] == 0xFF && pixels[2] == 0x0) { // green? + // PASSED. + } else if (pixels[0] == 0xFF && pixels[1] == 0xFF && pixels[2] == 0xFF) { // white? + reject("Failed, white detected, must be green"); + } else { + reject("Failed, readPixels (1) didn't work, gl error = " + gl.getError() + ", pixel = " +pixels[0] + " " +pixels[1] + " " +pixels[2]); + } + + // check if stencil worked, i.e. white pixels in the center + pixels[0] = pixels[1] = pixels[2] = pixels[3] = 20; + gl.readPixels(xrViewport.x + xrViewport.width / 2, xrViewport.y + xrViewport.height/2, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + if (pixels[0] == 0xFF && pixels[1] == 0xFF && pixels[2] == 0xFF) { // white? + // PASSED. + } else if (pixels[0] == 0x0 && pixels[1] == 0xFF && pixels[2] == 0x0) { // green? + reject("Failed, green detected, must be white"); + } else { + reject("Failed, readPixels (2) didn't work, gl error = " + gl.getError() + ", pixel = " +pixels[0] + " " +pixels[1] + " " +pixels[2]); + } + + // Finished. + resolve(); + }); +}); + +const gl_props = { antialias: false, alpha: false, stencil: true, depth: true }; + +// mac has issues with readPixels from the default fbo. +// skipping this test for Mac. +if (navigator.platform.indexOf("Mac") == -1) { + xr_session_promise_test( + nonImmersiveTestName, testFunction, fakeDeviceInitParams, 'inline', {}, {}, gl_props, gl_props); +} + +xr_session_promise_test( + immersiveTestName, testFunction, fakeDeviceInitParams, 'immersive-vr', {}, {}, gl_props, gl_props); + +</script> diff --git a/testing/web-platform/tests/webxr/xrWebGLLayer_viewports.https.html b/testing/web-platform/tests/webxr/xrWebGLLayer_viewports.https.html new file mode 100644 index 0000000000..8654e9e587 --- /dev/null +++ b/testing/web-platform/tests/webxr/xrWebGLLayer_viewports.https.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let immersiveTestName = "XRWebGLLayer reports a valid viewports for immersive sessions"; +let inlineTestName = "XRWebGLLayer reports a valid viewports for inline sessions"; + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let testFunction = function(session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer') + .then((space) => new Promise((resolve) => { + function onFrame(time, xrFrame) { + let viewer_pose = xrFrame.getViewerPose(space); + + let layer = xrFrame.session.renderState.baseLayer; + for (view of viewer_pose.views) { + let viewport = layer.getViewport(view); + + t.step(() => { + // Ensure the returned object is an XRViewport object + assert_not_equals(viewport, null); + assert_true(viewport instanceof XRViewport); + + // Ensure the viewport dimensions are valid + assert_greater_than_equal(viewport.x, 0); + assert_greater_than_equal(viewport.y, 0); + assert_greater_than_equal(viewport.width, 1); + assert_greater_than_equal(viewport.height, 1); + }); + + // Ensure none of the viewports overlap + for (other of viewer_pose.views) { + if (view !== other) { + let otherport = layer.getViewport(other); + let no_overlap = (viewport.x + viewport.width <= otherport.x) || + (otherport.x + otherport.width <= viewport.x) || + (viewport.y + viewport.height <= otherport.y) || + (otherport.y + otherport.height <= viewport.y); + + t.step(() => { + assert_true(no_overlap, "Overlap between viewport " + view.eye + " and " + other.eye); + }); + } + } + } + + // Finished test. + resolve(); + } + + session.requestAnimationFrame(onFrame); + })); +}; + +xr_session_promise_test(immersiveTestName, testFunction, + fakeDeviceInitParams, 'immersive-vr'); +xr_session_promise_test(immersiveTestName + ' with secondary views requested', testFunction, + fakeDeviceInitParams, 'immersive-vr', {'optionalFeatures': ['secondary-views']}); +xr_session_promise_test(inlineTestName, testFunction, + fakeDeviceInitParams, 'inline'); +xr_session_promise_test(inlineTestName + ' with secondary views requested', testFunction, + fakeDeviceInitParams, 'inline', {'optionalFeatures': ['secondary-views']}); +</script> diff --git a/testing/web-platform/tests/webxr/xr_viewport_scale.https.html b/testing/web-platform/tests/webxr/xr_viewport_scale.https.html new file mode 100644 index 0000000000..a3e3a4e5bd --- /dev/null +++ b/testing/web-platform/tests/webxr/xr_viewport_scale.https.html @@ -0,0 +1,231 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/webxr_test_constants.js"></script> +<script src="resources/webxr_util.js"></script> +<script src="resources/webxr_test_asserts.js"></script> + +<script> + +let fakeDeviceInitParams = TRACKED_IMMERSIVE_DEVICE; + +let isValidViewport = function(viewport) { + // Ensure the returned object is an XRViewport object + assert_not_equals(viewport, null); + assert_true(viewport instanceof XRViewport); + + // Ensure the viewport dimensions are valid + assert_greater_than_equal(viewport.x, 0); + assert_greater_than_equal(viewport.y, 0); + assert_greater_than_equal(viewport.width, 1); + assert_greater_than_equal(viewport.height, 1); +}; + +let containsViewport = function(outer, inner) { + assert_less_than_equal(inner.x, outer.x); + assert_less_than_equal(inner.y, outer.y); + assert_less_than_equal(inner.x + inner.width, outer.x + outer.width); + assert_less_than_equal(inner.y + inner.height, outer.y + outer.height); +}; + +let isSameViewport = function(a, b) { + return a.x == b.x && a.y == b.y && a.width == b.width && a.height == b.height; +}; + +let assertSameViewport = function(a, b) { + assert_equals(a.x, b.x, "viewport x should match"); + assert_equals(a.y, b.y, "viewport y should match"); + assert_equals(a.width, b.width, "viewport width should match"); + assert_equals(a.height, b.height, "viewport height should match"); +}; + +let testFunction = function(name, firstScale, nextFrame, session, fakeDeviceController, t) { + return session.requestReferenceSpace('viewer') + .then((space) => new Promise((resolve) => { + function onFrame(time, xrFrame1) { + let debug = xr_debug.bind(this, name); + debug('first frame'); + let layer = xrFrame1.session.renderState.baseLayer; + + let fullViewports = []; + + let views1 = xrFrame1.getViewerPose(space).views; + + for (view of views1) { + let viewport1a = layer.getViewport(view); + t.step(() => isValidViewport(viewport1a)); + fullViewports.push(viewport1a); + } + + // Now request a changed viewport scale. This must not change the + // viewports within this frame since they were already queried. + // If the UA supports viewport scaling, the change applies on the + // next frame. If it doesn't support viewport scaling, this call + // has no effect. + for (view of views1) { + view.requestViewportScale(firstScale); + } + + t.step(() => { + for (let i = 0; i < views1.length; ++i) { + let viewport1b = layer.getViewport(views1[i]); + assertSameViewport(viewport1b, fullViewports[i]); + } + }); + + if (nextFrame) { + session.requestAnimationFrame((time, xrFrame2) => + nextFrame(name, t, session, space, layer, fullViewports, xrFrame2, resolve)); + } else { + // test is done + resolve(); + } + } + + session.requestAnimationFrame(onFrame); + })); +}; + +let testViewportValid = function(name, t, session, space, layer, fullViewports, xrFrame, resolve) { + let debug = xr_debug.bind(this, name); + debug('second frame'); + let views = xrFrame.getViewerPose(space).views; + for (let i = 0; i < views.length; ++i) { + let viewport = layer.getViewport(views[i]); + t.step(() => isValidViewport(viewport)); + t.step(() => containsViewport(fullViewports[i], viewport)); + } + resolve(); +}; + +let testScaleAppliedNextFrame = function(name, t, session, space, layer, fullViewports, xrFrame, resolve) { + let debug = xr_debug.bind(this, name); + debug('second frame'); + let supportsScaling = false; + let views = xrFrame.getViewerPose(space).views; + for (let i = 0; i < views.length; ++i) { + let viewport = layer.getViewport(views[i]); + t.step(() => isValidViewport(viewport)); + t.step(() => containsViewport(fullViewports[i], viewport)); + if (!isSameViewport(fullViewports[i], viewport)) { + supportsScaling = true; + } + } + debug("supportsScaling=" + supportsScaling); + t.step(() => { + assert_implements_optional(supportsScaling, "requestViewportScale has no effect"); + }); + resolve(); +}; + +let testScaleSameFrame = function(name, t, session, space, layer, fullViewports, xrFrame, resolve) { + let debug = xr_debug.bind(this, name); + debug('second frame'); + let supportsScaling = false; + let views = xrFrame.getViewerPose(space).views; + let viewports2 = []; + for (let i = 0; i < views.length; ++i) { + let viewport2 = layer.getViewport(views[i]); + viewports2.push(viewport2); + if (!isSameViewport(fullViewports[i], viewport2)) { + supportsScaling = true; + } + } + debug("supportsScaling=" + supportsScaling); + if (!supportsScaling) { + // End the test early. + t.step(() => { + assert_implements_optional(false, "requestViewportScale has no effect"); + resolve(); + }); + } + + session.requestAnimationFrame((time, xrFrame3) => { + let views3 = xrFrame3.getViewerPose(space).views; + // Apply a new viewport scale before requesting viewports, + // this should take effect on the same frame. + for (view of views3) { + view.requestViewportScale(0.75); + } + for (let i = 0; i < views3.length; ++i) { + let viewport3 = layer.getViewport(views3[i]); + t.step(() => isValidViewport(viewport3)); + t.step(() => containsViewport(fullViewports[i], viewport3)); + t.step(() => containsViewport(viewport3, viewports2[i])); + t.step(() => { + // We don't know the exact expected size, but it should be in + // between the half-size and full-size viewports. + assert_false(isSameViewport(viewports2[i], viewport3)); + assert_false(isSameViewport(fullViewports[i], viewport3)); + }); + } + resolve(); + }); +}; + +let testRecommendedScale = function(name, t, session, space, layer, fullViewports, xrFrame, resolve) { + let debug = xr_debug.bind(this, name); + debug('second frame'); + let views = xrFrame.getViewerPose(space).views; + let haveRecommendedScale = false; + for (view of views) { + let recommended = view.recommendedViewportScale; + view.requestViewportScale(recommended); + if (recommended !== null && recommended !== undefined) { + haveRecommendedScale = true; + t.step(() => { + assert_greater_than(recommended, 0.0, "recommended scale invalid"); + assert_less_than_equal(recommended, 1.0, "recommended scale invalid"); + }); + } + } + t.step(() => { + assert_implements_optional(haveRecommendedScale, "recommendedViewportScale not provided"); + }); + for (let i = 0; i < views.length; ++i) { + let viewport = layer.getViewport(views[i]); + t.step(() => isValidViewport(viewport)); + t.step(() => containsViewport(fullViewports[i], viewport)); + } + resolve(); +}; + +for (let mode of ['inline', 'immersive-vr']) { + xr_session_promise_test( + "requestViewportScale valid viewport for " + mode + " session", + testFunction.bind(this, "valid viewport (0.5) " + mode, 0.5, testViewportValid), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "requestViewportScale valid viewport w/ null scale for " + mode + " session", + testFunction.bind(this, "valid viewport (null) " + mode, null, testViewportValid), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "requestViewportScale valid viewport w/ undefined scale for " + mode + " session", + testFunction.bind(this, "valid viewport (undefined) " + mode, null, testViewportValid), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "requestViewportScale valid viewport w/ very small scale for " + mode + " session", + testFunction.bind(this, "valid viewport (tiny) " + mode, 1e-6, testViewportValid), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "requestViewportScale applied next frame for " + mode + " session", + testFunction.bind(this, "scale applied next frame " + mode, 0.5, testScaleAppliedNextFrame), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "requestViewportScale same frame for " + mode + " session", + testFunction.bind(this, "same frame " + mode, 0.5, testScaleSameFrame), + fakeDeviceInitParams, + mode); + xr_session_promise_test( + "recommendedViewportScale for " + mode + " session", + testFunction.bind(this, "recommendedViewportScale " + mode, 0.5, testRecommendedScale), + fakeDeviceInitParams, + mode); +} + +</script> |