Bug 1622134 [wpt PR 22229] - WebXR - add WPTs for transient hit test API, a=testonly
authorPiotr Bialecki <bialpio@chromium.org>
Sat, 14 Mar 2020 11:30:59 +0000
changeset 518872 1f0d7ace89f6a8f45b184380e27c675719973899
parent 518871 20e91f15a8e23b490defbba4469b5fbe28d5f16d
child 518873 52841bab03e14c937c2ab4e53cc6667f190f5159
push id37217
push userccoroiu@mozilla.com
push dateSun, 15 Mar 2020 21:37:59 +0000
treeherdermozilla-central@f9fc9427476e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1622134, 22229, 2101713, 750277
milestone76.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1622134 [wpt PR 22229] - WebXR - add WPTs for transient hit test API, a=testonly Automatic update from web-platform-tests WebXR - add WPTs for transient hit test API Change-Id: I845e082ca413bcfa19a5ca4e06f587ebcb2cb254 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2101713 Commit-Queue: Piotr Bialecki <bialpio@chromium.org> Reviewed-by: Alexander Cooper <alcooper@chromium.org> Cr-Commit-Position: refs/heads/master@{#750277} -- wpt-commits: 9610643196e6b5eb25b14c33016121290886dac2 wpt-pr: 22229
testing/web-platform/tests/resources/chromium/webxr-test.js
testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states.https.html
testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html
testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html
testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html
--- a/testing/web-platform/tests/resources/chromium/webxr-test.js
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js
@@ -233,16 +233,18 @@ class MockRuntime {
 
     this.framesOfReference = {};
 
     this.input_sources_ = new Map();
     this.next_input_source_index_ = 1;
 
     // Currently active hit test subscriptons.
     this.hitTestSubscriptions_ = new Map();
+    // Currently active transient hit test subscriptions.
+    this.transientHitTestSubscriptions_ = new Map();
     // ID of the next subscription to be assigned.
     this.next_hit_test_id_ = 1;
 
     let supportedModes = [];
     if (fakeDeviceInit.supportedModes) {
       supportedModes = fakeDeviceInit.supportedModes.slice();
       if (fakeDeviceInit.supportedModes.length === 0) {
         supportedModes = ["inline"];
@@ -682,16 +684,35 @@ class MockRuntime {
     this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray });
 
     return Promise.resolve({
       result : device.mojom.SubscribeToHitTestResult.SUCCESS,
       subscriptionId : id
     });
   }
 
+  subscribeToHitTestForTransientInput(profileName, entityTypes, ray){
+    if (!this.supportedModes_.includes(device.mojom.XRSessionMode.kImmersiveAr)) {
+      // Reject outside of AR.
+      return Promise.resolve({
+        result : device.mojom.SubscribeToHitTestResult.FAILURE_GENERIC,
+        subscriptionId : 0
+      });
+    }
+
+    // Store the subscription information as-is:
+    const id = this.next_hit_test_id_++;
+    this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray });
+
+    return Promise.resolve({
+      result : device.mojom.SubscribeToHitTestResult.SUCCESS,
+      subscriptionId : id
+    });
+  }
+
   // Utility function
   requestRuntimeSession(sessionOptions) {
     return this.runtimeSupportsSession(sessionOptions).then((result) => {
       // The JavaScript bindings convert c_style_names to camelCase names.
       const options = new device.mojom.XRPresentationTransportOptions();
       options.transportMethod =
           device.mojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER;
       options.waitForTransferNotification = true;
@@ -764,30 +785,83 @@ class MockRuntime {
       return;
     }
 
     // Non-transient hit test:
     for (const [id, subscription] of this.hitTestSubscriptions_) {
       const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation);
       if (!mojo_from_native_origin) continue;
 
-      const ray_origin = {x: subscription.ray.origin.x, y: subscription.ray.origin.y, z: subscription.ray.origin.z, w: 1};
-      const ray_direction = {x: subscription.ray.direction.x, y: subscription.ray.direction.y, z: subscription.ray.direction.z, w: 0};
-
-      const mojo_ray_origin = XRMathHelper.transform_by_matrix(mojo_from_native_origin, ray_origin);
-      const mojo_ray_direction = XRMathHelper.transform_by_matrix(mojo_from_native_origin, ray_direction);
+      const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+        subscription.ray,
+        mojo_from_native_origin
+      );
 
       const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
 
       const result = new device.mojom.XRHitTestSubscriptionResultData();
       result.subscriptionId = id;
       result.hitTestResults = results;
 
       frameData.hitTestSubscriptionResults.results.push(result);
     }
+
+    // Transient hit test:
+    const mojo_from_viewer = this._getMojoFromViewer();
+
+    for (const [id, subscription] of this.transientHitTestSubscriptions_) {
+      const result = new device.mojom.XRHitTestTransientInputSubscriptionResultData();
+      result.subscriptionId = id;
+      result.inputSourceIdToHitTestResults = new Map();
+
+      // Find all input sources that match the profile name:
+      const matching_input_sources = Array.from(this.input_sources_.values())
+                                                        .filter(input_source => input_source.profiles_.includes(subscription.profileName));
+
+      for (const input_source of matching_input_sources) {
+        const mojo_from_native_origin = this._getMojoFromInputSource(mojo_from_viewer, input_source);
+
+        const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace(
+          subscription.ray,
+          mojo_from_native_origin
+        );
+
+        const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes);
+
+        result.inputSourceIdToHitTestResults.set(input_source.source_id_, results);
+      }
+
+      frameData.hitTestSubscriptionResults.transientInputResults.push(result);
+    }
+  }
+
+  // Returns 2-element array [origin, direction] of a ray in mojo space.
+  // |ray| is expressed relative to native origin.
+  _transformRayToMojoSpace(ray, mojo_from_native_origin) {
+    const ray_origin = {
+      x: ray.origin.x,
+      y: ray.origin.y,
+      z: ray.origin.z,
+      w: 1
+    };
+    const ray_direction = {
+      x: ray.direction.x,
+      y: ray.direction.y,
+      z: ray.direction.z,
+      w: 0
+    };
+
+    const mojo_ray_origin = XRMathHelper.transform_by_matrix(
+      mojo_from_native_origin,
+      ray_origin);
+    const mojo_ray_direction = XRMathHelper.transform_by_matrix(
+      mojo_from_native_origin,
+      ray_direction);
+
+    return [mojo_ray_origin, mojo_ray_direction];
   }
 
   // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data.
   _hitTestWorld(origin, direction, entityTypes) {
     let result = [];
 
     for (const region of this.world_.hitTestRegions) {
       const partial_result = this._hitTestRegion(
@@ -928,61 +1002,65 @@ class MockRuntime {
         hitResult.hitMatrix.matrix[15] = 1;
 
         return hitResult;
       }
     }
   }
 
   _getMojoFromInputSource(mojo_from_viewer, input_source) {
-    if(input_source.target_ray_mode_ === 'gaze') {  // XRTargetRayMode::GAZING
+    if (input_source.target_ray_mode_ === 'gaze') {  // XRTargetRayMode::GAZING
       // If the pointer origin is gaze, then the result is
       // just mojo_from_viewer.
       return mojo_from_viewer;
-    } else if(input_source.target_ray_mode_ === 'tracked-pointer') {  // XRTargetRayMode:::POINTING
+    } else if (input_source.target_ray_mode_ === 'tracked-pointer') {  // XRTargetRayMode:::POINTING
       // If the pointer origin is tracked-pointer, the result is just
       // mojo_from_input*input_from_pointer.
       return XRMathHelper.mul4x4(
         input_source.mojo_from_input_.matrix,
         input_source.input_from_pointer_.matrix);
-    } else if(input_source.target_ray_mode_ === 'screen') { // XRTargetRayMode::TAPPING
+    } else if (input_source.target_ray_mode_ === 'screen') { // XRTargetRayMode::TAPPING
       // If the pointer origin is screen, the input_from_pointer is
       // equivalent to viewer_from_pointer and the result is
       // mojo_from_viewer*viewer_from_pointer.
       return XRMathHelper.mul4x4(
         mojo_from_viewer,
         input_source.input_from_pointer_.matrix);
     } else {
-      return null
+      return null;
     }
   }
 
+  _getMojoFromViewer() {
+    const transform = {
+      position: [
+        this.pose_.position.x,
+        this.pose_.position.y,
+        this.pose_.position.z],
+      orientation: [
+        this.pose_.orientation.x,
+        this.pose_.orientation.y,
+        this.pose_.orientation.z,
+        this.pose_.orientation.w],
+    };
+
+    return getMatrixFromTransform(transform);
+  }
+
   _getMojoFromNativeOrigin(nativeOriginInformation) {
     const identity = function() {
       return [
         1, 0, 0, 0,
         0, 1, 0, 0,
         0, 0, 1, 0,
         0, 0, 0, 1
       ];
     };
 
-    const transform = {
-      position: [
-        this.pose_.position.x,
-        this.pose_.position.y,
-        this.pose_.position.z],
-      orientation: [
-        this.pose_.orientation.x,
-        this.pose_.orientation.y,
-        this.pose_.orientation.z,
-        this.pose_.orientation.w],
-    };
-
-    const mojo_from_viewer = getMatrixFromTransform(transform)
+    const mojo_from_viewer = this._getMojoFromViewer();
 
     if (nativeOriginInformation.$tag == device.mojom.XRNativeOriginInformation.Tags.inputSourceId) {
       if (!this.input_sources_.has(nativeOriginInformation.inputSourceId)) {
         return null;
       } else {
         const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceId);
         return this._getMojoFromInputSource(mojo_from_viewer, inputSource);
       }
@@ -1021,17 +1099,17 @@ class MockXRSessionMetricsRecorder {
 
 class MockXRInputSource {
   constructor(fakeInputSourceInit, id, pairedDevice) {
     this.source_id_ = id;
     this.pairedDevice_ = pairedDevice;
     this.handedness_ = fakeInputSourceInit.handedness;
     this.target_ray_mode_ = fakeInputSourceInit.targetRayMode;
 
-    if(fakeInputSourceInit.pointerOrigin == null) {
+    if (fakeInputSourceInit.pointerOrigin == null) {
       throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required.");
     }
 
     this.setPointerOrigin(fakeInputSourceInit.pointerOrigin);
     this.setProfiles(fakeInputSourceInit.profiles);
 
     this.primary_input_pressed_ = false;
     if (fakeInputSourceInit.selectionStarted != null) {
deleted file mode 100644
--- a/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states.https.html
+++ /dev/null
@@ -1,65 +0,0 @@
-<!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>
-<canvas />
-
-<script>
-
-const fakeDeviceInitParams = {
-  supportedModes: ["immersive-ar"],
-  views: VALID_VIEWS,
-  supportedFeatures: ALL_FEATURES,
-};
-
-// |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
-const testFunctionGenerator = function(shouldSucceed, endSession) {
-  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);
-        });
-      });
-    });
-  };
-
-  return testFunction;
-};
-
-xr_session_promise_test("Hit test subscription succeeds if the feature was requested",
-  testFunctionGenerator(/*shouldSucceed=*/true, /*endSession=*/false),
-  fakeDeviceInitParams,
-  'immersive-ar', { 'requiredFeatures': ['hit-test'] });
-
-xr_session_promise_test("Hit test subscription fails if the feature was not requested",
-  testFunctionGenerator(/*shouldSucceed=*/false, /*endSession=*/false),
-  fakeDeviceInitParams,
-  'immersive-ar', {});
-
-xr_session_promise_test("Hit test subscription fails if the feature was requested but the session already ended",
-  testFunctionGenerator(/*shouldSucceed=*/false, /*endSession=*/true),
-  fakeDeviceInitParams,
-  'immersive-ar', { 'requiredFeatures': ['hit-test'] });
-
-</script>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_regular.https.html
@@ -0,0 +1,70 @@
+<!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>
+<canvas />
+
+<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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_states_transient.https.html
@@ -0,0 +1,67 @@
+<!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>
+<canvas />
+
+<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>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/hit-test/ar_hittest_subscription_transientInputSources.https.html
@@ -0,0 +1,173 @@
+<!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>
+<canvas />
+
+<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 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 input_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) {
+    return session.requestReferenceSpace('local').then((localRefSpace) => new Promise((resolve, reject) => {
+
+      const input_source_controller = fakeDeviceController.simulateInputSourceConnection(screen_controller_init);
+
+      session.requestAnimationFrame((time, frame) => {
+        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) => {
+            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) => {
+              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: ");
+                  }
+                }
+              });
+
+              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, 0, 0],      // middle of the screen
+  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, 0, 0],      // middle of the screen
+  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>