summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webxr
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/webxr
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webxr')
-rw-r--r--testing/web-platform/tests/webxr/META.yml3
-rw-r--r--testing/web-platform/tests/webxr/anchors/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_create_move.https.html94
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_delay_creation.https.html122
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_failure.https.html72
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_freefloating_pause_resume_stop.https.html115
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_getAnchors.https.html58
-rw-r--r--testing/web-platform/tests/webxr/anchors/ar_anchor_states.https.html114
-rw-r--r--testing/web-platform/tests/webxr/anchors/idlharness.https.window.js16
-rw-r--r--testing/web-platform/tests/webxr/ar-module/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/ar-module/idlharness.https.window.js17
-rw-r--r--testing/web-platform/tests/webxr/ar-module/xrDevice_isSessionSupported_immersive-ar.https.html34
-rw-r--r--testing/web-platform/tests/webxr/ar-module/xrDevice_requestSession_immersive-ar.https.html27
-rw-r--r--testing/web-platform/tests/webxr/ar-module/xrSession_environmentBlendMode.https.html23
-rw-r--r--testing/web-platform/tests/webxr/ar-module/xrSession_interactionMode.https.html84
-rw-r--r--testing/web-platform/tests/webxr/camera-access/xrCamera_resolution.https.html80
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_dataUnavailable.https.html27
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_inactiveFrame.https.html26
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_incorrectUsage.https.html48
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_luminance_alpha_dataValid.https.html110
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/cpu/depth_sensing_cpu_staleView.https.html26
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/dataUnavailableTests.js58
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/depth_sensing_notEnabled.https.html61
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_dataUnavailable.https.html27
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_inactiveFrame.https.html26
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_incorrectUsage.https.html46
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/gpu/depth_sensing_gpu_staleView.https.html26
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/inactiveFrameTests.js36
-rw-r--r--testing/web-platform/tests/webxr/depth-sensing/staleViewsTests.js39
-rw-r--r--testing/web-platform/tests/webxr/dom-overlay/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay.https.html298
-rw-r--r--testing/web-platform/tests/webxr/dom-overlay/ar_dom_overlay_hit_test.https.html128
-rw-r--r--testing/web-platform/tests/webxr/dom-overlay/idlharness.https.window.js20
-rw-r--r--testing/web-platform/tests/webxr/dom-overlay/nested_fullscreen.https.html81
-rw-r--r--testing/web-platform/tests/webxr/events_input_source_recreation.https.html143
-rw-r--r--testing/web-platform/tests/webxr/events_input_sources_change.https.html113
-rw-r--r--testing/web-platform/tests/webxr/events_referenceSpace_reset_immersive.https.html50
-rw-r--r--testing/web-platform/tests/webxr/events_referenceSpace_reset_inline.https.html52
-rw-r--r--testing/web-platform/tests/webxr/events_session_select.https.html114
-rw-r--r--testing/web-platform/tests/webxr/events_session_select_subframe.https.html65
-rw-r--r--testing/web-platform/tests/webxr/events_session_squeeze.https.html136
-rw-r--r--testing/web-platform/tests/webxr/exclusive_requestFrame_nolayer.https.html56
-rw-r--r--testing/web-platform/tests/webxr/gamepads-module/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/gamepads-module/idlharness.https.window.js16
-rw-r--r--testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_disconnect.https.html159
-rw-r--r--testing/web-platform/tests/webxr/gamepads-module/xrInputSource_gamepad_input_registered.https.html130
-rw-r--r--testing/web-platform/tests/webxr/getInputPose_handedness.https.html83
-rw-r--r--testing/web-platform/tests/webxr/getInputPose_pointer.https.html96
-rw-r--r--testing/web-platform/tests/webxr/getViewerPose_emulatedPosition.https.html55
-rw-r--r--testing/web-platform/tests/webxr/hand-input/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/hand-input/idlharness.https.window.js16
-rw-r--r--testing/web-platform/tests/webxr/hit-test/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_source_cancel.https.html100
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_inputSources.https.html170
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_refSpaces.https.html158
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html69
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html66
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html177
-rw-r--r--testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_unlocalizable.https.html108
-rw-r--r--testing/web-platform/tests/webxr/hit-test/idlharness.https.html29
-rw-r--r--testing/web-platform/tests/webxr/hit-test/xrRay_constructor.https.html159
-rw-r--r--testing/web-platform/tests/webxr/hit-test/xrRay_matrix.https.html101
-rw-r--r--testing/web-platform/tests/webxr/idlharness.https.window.js55
-rw-r--r--testing/web-platform/tests/webxr/layers/META.yml1
-rw-r--r--testing/web-platform/tests/webxr/layers/xrWebGLBinding_constructor.https.html83
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_oldSession.https.html57
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_staleFrame.https.html47
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrFrame_getLightEstimate_valid.https.html104
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_ended.https.html21
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_notEnabled.https.html18
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrSession_getLightProbe_valid.https.html27
-rw-r--r--testing/web-platform/tests/webxr/light-estimation/xrWebGLBinding_getReflectionCubeMap.https.html95
-rw-r--r--testing/web-platform/tests/webxr/navigator_xr_sameObject.https.html32
-rw-r--r--testing/web-platform/tests/webxr/render_state_update.https.html95
-rw-r--r--testing/web-platform/tests/webxr/render_state_vertical_fov_immersive.https.html41
-rw-r--r--testing/web-platform/tests/webxr/render_state_vertical_fov_inline.https.html89
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_check.html17
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_math_utils.js67
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_asserts.js185
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants.js204
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_depth.js78
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_test_constants_fake_world.js79
-rw-r--r--testing/web-platform/tests/webxr/resources/webxr_util.js241
-rw-r--r--testing/web-platform/tests/webxr/webGLCanvasContext_create_xrcompatible.https.html52
-rw-r--r--testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_contextlost.https.html28
-rw-r--r--testing/web-platform/tests/webxr/webGLCanvasContext_makecompatible_reentrant.https.html48
-rw-r--r--testing/web-platform/tests/webxr/webxr-supported-by-feature-policy.html10
-rw-r--r--testing/web-platform/tests/webxr/webxr_availability.http.sub.html39
-rw-r--r--testing/web-platform/tests/webxr/webxr_feature_policy.https.html97
-rw-r--r--testing/web-platform/tests/webxr/webxr_feature_policy.https.html.headers1
-rw-r--r--testing/web-platform/tests/webxr/xrBoundedReferenceSpace_updates.https.html58
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_disconnect_ends.https.html38
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive.https.html21
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_isSessionSupported_immersive_unsupported.https.html21
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_isSessionSupported_inline.https.html22
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html26
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_no_gesture.https.html16
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_unsupported.https.html23
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_no_mode.https.html22
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html16
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_optionalFeatures.https.html32
-rw-r--r--testing/web-platform/tests/webxr/xrDevice_requestSession_requiredFeatures_unknown.https.html35
-rw-r--r--testing/web-platform/tests/webxr/xrFrame_getPose.https.html101
-rw-r--r--testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose.https.html62
-rw-r--r--testing/web-platform/tests/webxr/xrFrame_getViewerPose_getPose_identities.https.html113
-rw-r--r--testing/web-platform/tests/webxr/xrFrame_lifetime.https.html57
-rw-r--r--testing/web-platform/tests/webxr/xrFrame_session_sameObject.https.html25
-rw-r--r--testing/web-platform/tests/webxr/xrInputSource_add_remove.https.html68
-rw-r--r--testing/web-platform/tests/webxr/xrInputSource_emulatedPosition.https.html62
-rw-r--r--testing/web-platform/tests/webxr/xrInputSource_getPose_targetRay_grip.https.html50
-rw-r--r--testing/web-platform/tests/webxr/xrInputSource_profiles.https.html38
-rw-r--r--testing/web-platform/tests/webxr/xrInputSource_sameObject.https.html62
-rw-r--r--testing/web-platform/tests/webxr/xrPose_transform_sameObject.https.html42
-rw-r--r--testing/web-platform/tests/webxr/xrReferenceSpace_originOffset.https.html135
-rw-r--r--testing/web-platform/tests/webxr/xrReferenceSpace_originOffsetBounded.https.html244
-rw-r--r--testing/web-platform/tests/webxr/xrReferenceSpace_originOffset_viewer.https.html54
-rw-r--r--testing/web-platform/tests/webxr/xrReferenceSpace_relationships.https.html58
-rw-r--r--testing/web-platform/tests/webxr/xrRigidTransform_constructor.https.html148
-rw-r--r--testing/web-platform/tests/webxr/xrRigidTransform_inverse.https.html108
-rw-r--r--testing/web-platform/tests/webxr/xrRigidTransform_matrix.https.html45
-rw-r--r--testing/web-platform/tests/webxr/xrRigidTransform_sameObject.https.html40
-rw-r--r--testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html55
-rw-r--r--testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html42
-rw-r--r--testing/web-platform/tests/webxr/xrSession_enabledFeatures.https.html87
-rw-r--r--testing/web-platform/tests/webxr/xrSession_end.https.html40
-rw-r--r--testing/web-platform/tests/webxr/xrSession_features_deviceSupport.https.html60
-rw-r--r--testing/web-platform/tests/webxr/xrSession_input_events_end.https.html79
-rw-r--r--testing/web-platform/tests/webxr/xrSession_prevent_multiple_exclusive.https.html43
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.https.html32
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html57
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getViewerPose.https.html84
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_timestamp.https.html93
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestReferenceSpace.https.html70
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestReferenceSpace_features.https.html82
-rw-r--r--testing/web-platform/tests/webxr/xrSession_requestSessionDuringEnd.https.html68
-rw-r--r--testing/web-platform/tests/webxr/xrSession_sameObject.https.html66
-rw-r--r--testing/web-platform/tests/webxr/xrSession_viewer_availability.https.html73
-rw-r--r--testing/web-platform/tests/webxr/xrSession_viewer_referenceSpace.https.html73
-rw-r--r--testing/web-platform/tests/webxr/xrSession_visibilityState.https.html82
-rw-r--r--testing/web-platform/tests/webxr/xrStationaryReferenceSpace_floorlevel_updates.https.html65
-rw-r--r--testing/web-platform/tests/webxr/xrView_eyes.https.html45
-rw-r--r--testing/web-platform/tests/webxr/xrView_match.https.html87
-rw-r--r--testing/web-platform/tests/webxr/xrView_oneframeupdate.https.html86
-rw-r--r--testing/web-platform/tests/webxr/xrView_sameObject.https.html37
-rw-r--r--testing/web-platform/tests/webxr/xrViewerPose_secondaryViews.https.html94
-rw-r--r--testing/web-platform/tests/webxr/xrViewerPose_views_sameObject.https.html33
-rw-r--r--testing/web-platform/tests/webxr/xrViewport_valid.https.html76
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_constructor.https.html82
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_draw.https.html92
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_sameObject.https.html25
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_framebuffer_scale.https.html58
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer.https.html124
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_opaque_framebuffer_stencil.https.html223
-rw-r--r--testing/web-platform/tests/webxr/xrWebGLLayer_viewports.https.html69
-rw-r--r--testing/web-platform/tests/webxr/xr_viewport_scale.https.html231
155 files changed, 10735 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/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..1a0d7bddad
--- /dev/null
+++ b/testing/web-platform/tests/webxr/resources/webxr_test_constants.js
@@ -0,0 +1,204 @@
+// 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',
+];
+
+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_feature_policy.https.html b/testing/web-platform/tests/webxr/webxr_feature_policy.https.html
new file mode 100644
index 0000000000..b493ec73cc
--- /dev/null
+++ b/testing/web-platform/tests/webxr/webxr_feature_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 feature 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 feature policy"),
+
+ promise_rejects_dom(t, "NotSupportedError",
+ navigator.xr.requestSession("immersive-vr"),
+ "Immersive-vr should reject without feature 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 feature policy");
+ });
+
+ return promise_rejects_dom(t, "SecurityError",
+ gl.makeXRCompatible(),
+ "makeXRCompatible should reject without feature policy");
+});
+</script>
diff --git a/testing/web-platform/tests/webxr/webxr_feature_policy.https.html.headers b/testing/web-platform/tests/webxr/webxr_feature_policy.https.html.headers
new file mode 100644
index 0000000000..2c75896233
--- /dev/null
+++ b/testing/web-platform/tests/webxr/webxr_feature_policy.https.html.headers
@@ -0,0 +1 @@
+Feature-Policy: xr-spatial-tracking 'none'
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>