WebXR Test API

Editor’s Draft,

This version:
https://immersive-web.github.io/webxr-test-api/
Editors:
(Mozilla)
(Google)
Testing-only API

The API represented in this document is for testing only and should not be exposed to users.


Abstract

The WebXR Test API module provides a mocking interface for Web Platform Tests to be able to test the WebXR Device API.

Status of this document

This section describes the status of this document at the time of its publication. Other documents may supersede this document. A list of current W3C publications and the latest revision of this technical report can be found in the W3C technical reports index at http://www.w3.org/TR/.

This document was published by the Immersive Web Working Group as an Editors' Draft. This document is intended to become a W3C Recommendation. Feedback and comments on this specification are welcome. Please use Github issues. Discussions may also be found in the public-immersive-web@w3.org archives.

Publication as an Editors' Draft does not imply endorsement by the W3C Membership. This is a draft document and may be updated, replaced or obsoleted by other documents at any time. It is inappropriate to cite this document as other than work in progress.

This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent which the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

This document is governed by the 1 March 2019 W3C Process Document.

1. Introduction

In order to allow Web Platform Tests for WebXR there are some basic functions which are common across all tests, such as adding a fake test device and specifying poses. Below is an API which attempts to capture the necessary functions, based off what was defined in the spec. Different browser vendors can implement this API in whatever way is most compatible with their browser. For example, some browsers may back the interface with a WebDriver API while others may use HTTP or IPC mechanisms to communicate with an out of process fake backend.

These initialization object and control interfaces do not represent a complete set of WebXR functionality, and are expected to be expanded on as the WebXR spec grows.

2. Conformance

Interfaces and functionality exposed by this specification SHOULD NOT be exposed to typical browsing experiences, and instead SHOULD only be used when running Web Platform Tests.

3. Simulated devices

This API gives tests the ability to spin up a simulated XR device which is an XR device which from the point of view of the WebXR API behaves like a normal XR device. These simulated XR devices can be controlled by the associated FakeXRDevice object.

Every simulated XR device may have an native bounds geometry which is an array of DOMPointReadOnlys, used to initialize the native bounds geometry of any XRBoundedReferenceSpaces created for the device. If null, the device is treated as if it is not currently tracking a bounded reference space.

Every simulated XR device may have a floor origin which is an XRRigidTransform used to note the position of the physical floor. If null, the device is treated as if it is unable to identify the physical floor.

Every simulated XR device may have an viewer origin which is an XRRigidTransform used to set the position and orientation of the viewer. If null, the device is treated as if it has lost tracking.

Every simulated XR device has an emulated position boolean which is a boolean used to set the emulatedPosition of any XRPoses produced involving the viewer. This is initially false.

Every simulated XR device has an visibility state which is an XRVisibilityState used to set the visibilityState of any XRSessions associated with the simulated XR device . This is initially "visible". When it is changed, the associated changes must be reflected on the XRSession, including triggering onvisibilitychange events if necessary.

Every view for a simulated XR device has an associated device resolution, which is an instance of FakeXRDeviceResolution. This resolution must be used when constructing XRViewport values for the view, based on the canvas size.

Every view for a simulated XR device may have an associated field of view, which is an instance of FakeXRFieldOfViewInit used to calculate projection matrices using depth values. If the field of view is set, projection matrix values are calculated using the field of view and depthNear and depthFar values.

The WebXR API never exposes native origins directly, instead exposing transforms between them, so we need to specify a base reference space for FakeXRRigidTransformInits so that we can have consistent numerical values across implementations. When used as an origin, FakeXRRigidTransformInits are in the base reference space where the viewer's native origin is identity at initialization, unless otherwise specified. In this space, the "local" reference space has a native origin of identity. This is an arbitrary choice: changing this reference space doesn’t affect the data returned by the WebXR API, but we must make such a choice so that the tests produce the same results across different UAs. When used as an origin it is logically a transform from the origin’s space to the underlying base reference space described above.

4. Initialization

4.1. navigator.xr.test

partial interface XR {
    [SameObject] readonly attribute XRTest test;
};

The test attribute’s getter MUST return the XRTest object that is associated with it. This object MAY be lazily created.

4.2. XRTest

The XRTest object is the entry point for all testing.

interface XRTest {
  Promise<FakeXRDevice> simulateDeviceConnection(FakeXRDeviceInit init);
  void simulateUserActivation(Function f);
  Promise<void> disconnectAllDevices();
};
The simulateDeviceConnection(init) method creates a new simulated XR device.

When this method is invoked, the user agent MUST run the following steps:

  1. Let promise be a new Promise.

  2. Run the following steps in parallel:

    1. Let device be a new simulated XR device.

    2. For each view in init’s views:

      1. Let v be the result of running parse a view on view.

      2. If running parse a view threw an error, reject promise with this error and abort these steps.

      3. Append v to device’s list of views.

    3. If init’s supportedFeatures is set, set device’s list of features it is capable of supporting to init’s supportedFeatures.

    4. If init’s boundsCoordinates is set, perform the following steps:

      1. If init’s boundsCoordinates has less than 3 elements, reject promise with TypeError and abort these steps.

      2. Set device’s native bounds geometry to init’s boundsCoordinates.

    5. If init’s floorOrigin is set, set device’s floor origin to init’s floorOrigin.

    6. If init’s viewerOrigin is set, set device’s viewer origin to init’s viewerOrigin.

    7. Let supportedModes be an empty list of XRSessionModes.

    8. Modify supportedModes as follows:

      If init’s supportedModes is present:
      1. Append the contents of init’s supportedModes to supportedModes.

      2. If supportedModes is empty, append "inline" to it.

      Else
      1. Append "inline" to supportedModes.

      2. If init’s supportsImmersive is true, append "immersive-vr" to supportedModes.

    9. Set device’s list of supported modes to supportedModes.

    10. Register device based on the following:

      1. If supportedModes contains "immersive-vr" or "immersive-ar", append device to the xr's list of immersive XR devices.

      2. If supportedModes contains "inline", set the inline XR device to device.

    11. Let d be a new FakeXRDevice object with device as device.

    12. Resolve promise with d.

  3. Return promise.

When simulateUserActivation(f) is called, invoke f as if it was triggered by user activation.

When disconnectAllDevices() is called, remove all simulated XR devices from the context object's XR object’s list of immersive XR devices as if they were disconnected. If the inline XR device is a simulated XR device, reset it to the default inline XR device.

4.3. FakeXRDeviceInit

dictionary FakeXRDeviceInit {
    required boolean supportsImmersive;
    sequence<XRSessionMode> supportedModes;
    required sequence<FakeXRViewInit> views;

    sequence<any> supportedFeatures;
    sequence<FakeXRBoundsPoint> boundsCoordinates;
    FakeXRRigidTransformInit floorOrigin;
    FakeXRRigidTransformInit viewerOrigin;

    // Hit test extensions:
    FakeXRWorldInit world;
};

dictionary FakeXRViewInit {
  required XREye eye;
  required sequence<float> projectionMatrix;
  required FakeXRDeviceResolution resolution;
  required FakeXRRigidTransformInit viewOffset;
  FakeXRFieldOfViewInit fieldOfView;
};

dictionary FakeXRFieldOfViewInit {
  required float upDegrees;
  required float downDegrees;
  required float leftDegrees;
  required float rightDegrees;
};

dictionary FakeXRDeviceResolution {
    required long width;
    required long height;
};

dictionary FakeXRBoundsPoint {
  double x; double z;
};

dictionary FakeXRRigidTransformInit {
  required sequence<float> position;
  required sequence<float> orientation;
};

The supportsImmersive is deprecated in favor of supportedModes and will be removed in future revisions of the specification.

To parse a rigid transform given a FakeXRRigidTransformInit init, perform the following steps:
  1. Let p be init’s position.

  2. If p does not have three elements, throw a TypeError.

  3. Let o be init’s orientation.

  4. If o does not have four elements, throw a TypeError.

  5. Let position be a DOMPointInit with x, y and z equal to the three elements of p in order, and w equal to 1.

  6. Let orientation be a DOMPointInit with x, y, z, and w equal to the four elements of o in order.

  7. Construct an XRRigidTransform transform with position position and orientation orientation.

  8. Return transform.

To parse a view given a FakeXRViewInit init, perform the following steps:
  1. Let view be a new view.

  2. Set view’s eye to init’s eye.

  3. If init’s projectionMatrix does not have 16 elements, throw a TypeError.

  4. Set view’s projection matrix to init’s projectionMatrix.

  5. Set view’s view offset to the result of running parse a rigid transform init’s viewOffset.

  6. Set view’s device resolution to init’s resolution.

  7. If init’s fieldOfView is set, perform the following steps:

    1. Set view’s field of view to init’s fieldOfView.

    2. Set view’s projection matrix to the projection matrix corresponding to this field of view, and depth values equal to depthNear and depthFar of any XRSession associated with the device. If there currently is none, use the default values of near=0.1, far=1000.0.

  8. Set view’s projection matrix to init’s projectionMatrix.

  9. Return view.

5. Mocking

5.1. FakeXRDevice

interface FakeXRDevice {
  void setViews(sequence<FakeXRViewInit> views);

  Promise<void> disconnect();

  void setViewerOrigin(FakeXRRigidTransformInit origin, optional boolean emulatedPosition = false);
  void clearViewerOrigin();
  void setFloorOrigin(FakeXRRigidTransformInit origin);
  void clearFloorOrigin();
  void setBoundsGeometry(sequence<FakeXRBoundsPoint> boundsCoordinates);
  void simulateResetPose();

  void simulateVisibilityChange(XRVisibilityState state);

  FakeXRInputController simulateInputSourceConnection(FakeXRInputSourceInit init);

  // Hit test extensions:
  void setWorld(FakeXRWorldInit world);
  void clearWorld();
};

Each FakeXRDevice object has an associated device, which is a simulated XR device that it is able to control.

Operations on the FakeXRDevice's device typically take place on the next animation frame, i.e. they are not immediately observable until a future requestAnimationFrame() callback.

To determine when this frame is, for a given operation, choose a frame based on the following:

If such an operation is triggered within an XR animation frame:
Choose the next XR animation frame, whenever it may occur
If such an operation is triggered outside of an XR animation frame:
Choose a frame based on the following:
If there are no callbacks in the list of animation frame callbacks:
Choose the next XR animation frame, whenever it may occur
Otherwise:
Choose the next-to-next XR animation frame, whenever it may occur

NOTE: The reason we defer an extra frame when there are pending animation frame callbacks is to avoid having to deal with potential race conditions when the device is ready to trigger an animation frame callback, but has not yet. In practice, this means that tests should be written so that they wait until they have performed all such operations before calling the next requestAnimationFrame()

The setViews(views) method performs the following steps:
  1. On the next animation frame, run the following steps:

    1. Let l be an empty list.

    2. For each view in views:

      1. Let v be the result of running parse a view on view.

      2. Append v to l.

    3. Set device's list of views to l.

When disconnect() method is called, perform the following steps:

  1. Remove device from the context object's XR object’s list of immersive XR devices as if it were disconnected.

  2. If the inline XR device is equal to the FakeXRDevice, reset it to the default inline XR device.

The setViewerOrigin(origin, emulatedPosition) performs the following steps:
  1. Let o be the result of running parse a rigid transform on origin.

  2. On the next animation frame, perform the following steps:

    1. Set device's viewer origin to o.

    2. Set device's emulated position boolean to emulatedPosition.

The clearViewerOrigin() method will, on the next animation frame, set device's viewer origin to null.

The simulateVisibilityChange(state) method will, as soon as possible, set device's visibility state to state.

The setFloorOrigin(origin) performs the following steps:
  1. Let o be the result of running parse a rigid transform on origin.

  2. On the next animation frame, set device's floor origin to o.

The clearFloorOrigin() method will, on the next animation frame, set device's floor origin to null.

The setBoundsGeometry(boundsCoordinates) performs the following steps:
  1. If boundsCoordinates has fewer than 3 elements, throw a TypeError.

  2. On the next animation frame, set device's native bounds geometry to boundsCoordinates.

The simulateResetPose() method will, as soon as possible, behave as if the device's viewer's native origin had a discontinuity, triggering appropriate reset events.

5.2. FakeXRInputController

dictionary FakeXRInputSourceInit {
  required XRHandedness handedness;
  required XRTargetRayMode targetRayMode;
  required FakeXRRigidTransformInit pointerOrigin;
  required sequence<DOMString> profiles;
  boolean selectionStarted = false;
  boolean selectionClicked = false;
  sequence<FakeXRButtonStateInit> supportedButtons;
  FakeXRRigidTransformInit gripOrigin;
};

interface FakeXRInputController {
  void setHandedness(XRHandedness handedness);
  void setTargetRayMode(XRTargetRayMode targetRayMode);
  void setProfiles(sequence<DOMString> profiles);
  void setGripOrigin(FakeXRRigidTransformInit gripOrigin, optional boolean emulatedPosition = false);
  void clearGripOrigin();
  void setPointerOrigin(FakeXRRigidTransformInit pointerOrigin, optional boolean emulatedPosition = false);

  void disconnect();
  void reconnect();

  void startSelection();
  void endSelection();
  void simulateSelect();

  void setSupportedButtons(sequence<FakeXRButtonStateInit> supportedButtons);
  void updateButtonState(FakeXRButtonStateInit buttonState);
};

enum FakeXRButtonType {
  "grip",
  "touchpad",
  "thumbstick",
  "optional-button",
  "optional-thumbstick"
};

dictionary FakeXRButtonStateInit {
  required FakeXRButtonType buttonType;
  required boolean pressed;
  required boolean touched;
  required float pressedValue;
  float xValue = 0.0;
  float yValue = 0.0;
};

6. Hit test extensions

The hit test extensions for test API SHOULD be implemented by all user agents that implement WebXR Hit Test Module.

dictionary FakeXRWorldInit {
  required sequence<FakeXRRegionInit> hitTestRegions;
};

FakeXRWorldInit dictionary describes the state of the world that will be used when computing hit test results on a FakeXRDevice.

hitTestRegions contains a collection of FakeXRRegionInits that are used to describe specific regions of the fake world. The order of the regions does not matter.

dictionary FakeXRRegionInit {
  required sequence<FakeXRTriangleInit> faces;
  required FakeXRRegionType type;
};

FakeXRRegionInit dictionary describes the contents of a specific region of the world.

faces contains a collection of FakeXRTriangleInits that enumerate all the faces contained by the region. The order of the faces does not matter.

type contains a type of the region that will be used during computation of hit test results.

dictionary FakeXRTriangleInit {
  required sequence<DOMPointInit> vertices;  // size = 3
};

FakeXRTriangleInit dictionary describes a single face of a region.

vertices contains a collection of DOMPointInits that comprise the face. The face will be considered as solid when computing hit test results and as such, the winding order of the vertices does not matter.

enum FakeXRRegionType {
  "point",
  "plane",
  "mesh"
};

FakeXRRegionType enum is used to describe a type of the world region.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[GEOMETRY-1]
Simon Pieters; Chris Harrelson. Geometry Interfaces Module Level 1. 4 December 2018. CR. URL: https://www.w3.org/TR/geometry-1/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[PROMISES-GUIDE]
Domenic Denicola. Writing Promise-Using Specifications. 9 November 2018. TAG Finding. URL: https://www.w3.org/2001/tag/doc/promises-guide
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

IDL Index

partial interface XR {
    [SameObject] readonly attribute XRTest test;
};

interface XRTest {
  Promise<FakeXRDevice> simulateDeviceConnection(FakeXRDeviceInit init);
  void simulateUserActivation(Function f);
  Promise<void> disconnectAllDevices();
};

dictionary FakeXRDeviceInit {
    required boolean supportsImmersive;
    sequence<XRSessionMode> supportedModes;
    required sequence<FakeXRViewInit> views;

    sequence<any> supportedFeatures;
    sequence<FakeXRBoundsPoint> boundsCoordinates;
    FakeXRRigidTransformInit floorOrigin;
    FakeXRRigidTransformInit viewerOrigin;

    // Hit test extensions:
    FakeXRWorldInit world;
};

dictionary FakeXRViewInit {
  required XREye eye;
  required sequence<float> projectionMatrix;
  required FakeXRDeviceResolution resolution;
  required FakeXRRigidTransformInit viewOffset;
  FakeXRFieldOfViewInit fieldOfView;
};

dictionary FakeXRFieldOfViewInit {
  required float upDegrees;
  required float downDegrees;
  required float leftDegrees;
  required float rightDegrees;
};

dictionary FakeXRDeviceResolution {
    required long width;
    required long height;
};

dictionary FakeXRBoundsPoint {
  double x; double z;
};

dictionary FakeXRRigidTransformInit {
  required sequence<float> position;
  required sequence<float> orientation;
};


interface FakeXRDevice {
  void setViews(sequence<FakeXRViewInit> views);

  Promise<void> disconnect();

  void setViewerOrigin(FakeXRRigidTransformInit origin, optional boolean emulatedPosition = false);
  void clearViewerOrigin();
  void setFloorOrigin(FakeXRRigidTransformInit origin);
  void clearFloorOrigin();
  void setBoundsGeometry(sequence<FakeXRBoundsPoint> boundsCoordinates);
  void simulateResetPose();

  void simulateVisibilityChange(XRVisibilityState state);

  FakeXRInputController simulateInputSourceConnection(FakeXRInputSourceInit init);

  // Hit test extensions:
  void setWorld(FakeXRWorldInit world);
  void clearWorld();
};


dictionary FakeXRInputSourceInit {
  required XRHandedness handedness;
  required XRTargetRayMode targetRayMode;
  required FakeXRRigidTransformInit pointerOrigin;
  required sequence<DOMString> profiles;
  boolean selectionStarted = false;
  boolean selectionClicked = false;
  sequence<FakeXRButtonStateInit> supportedButtons;
  FakeXRRigidTransformInit gripOrigin;
};

interface FakeXRInputController {
  void setHandedness(XRHandedness handedness);
  void setTargetRayMode(XRTargetRayMode targetRayMode);
  void setProfiles(sequence<DOMString> profiles);
  void setGripOrigin(FakeXRRigidTransformInit gripOrigin, optional boolean emulatedPosition = false);
  void clearGripOrigin();
  void setPointerOrigin(FakeXRRigidTransformInit pointerOrigin, optional boolean emulatedPosition = false);

  void disconnect();
  void reconnect();

  void startSelection();
  void endSelection();
  void simulateSelect();

  void setSupportedButtons(sequence<FakeXRButtonStateInit> supportedButtons);
  void updateButtonState(FakeXRButtonStateInit buttonState);
};

enum FakeXRButtonType {
  "grip",
  "touchpad",
  "thumbstick",
  "optional-button",
  "optional-thumbstick"
};

dictionary FakeXRButtonStateInit {
  required FakeXRButtonType buttonType;
  required boolean pressed;
  required boolean touched;
  required float pressedValue;
  float xValue = 0.0;
  float yValue = 0.0;
};

dictionary FakeXRWorldInit {
  required sequence<FakeXRRegionInit> hitTestRegions;
};


dictionary FakeXRRegionInit {
  required sequence<FakeXRTriangleInit> faces;
  required FakeXRRegionType type;
};


dictionary FakeXRTriangleInit {
  required sequence<DOMPointInit> vertices;  // size = 3
};


enum FakeXRRegionType {
  "point",
  "plane",
  "mesh"
};