Bug 1485476 [wpt PR 12629] - Reland "Move XR LayoutTests over to be WPTs", a=testonly
authorAnna Offenwanger <offenwanger@chromium.org>
Fri, 24 Aug 2018 13:00:29 +0000
changeset 491611 81368bd086e67a5903960f94c75a754c44a28b41
parent 491610 4168bd07e9a32b9376e30ce434735b036aa7499b
child 491612 68480db895db2446ce2b6e60b79d21ae9cd87597
push id1815
push userffxbld-merge
push dateMon, 15 Oct 2018 10:40:45 +0000
treeherdermozilla-release@18d4c09e9378 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs1485476, 12629, 863557, 844772, 1171857, 585064, 1185384, 585342
milestone63.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 1485476 [wpt PR 12629] - Reland "Move XR LayoutTests over to be WPTs", a=testonly Automatic update from web-platform-testsReland "Move XR LayoutTests over to be WPTs" This is a reland of f4c510e1cb2ccdc0ce07b060be9ad52f8eda5cb8 Original change's description: > Move XR LayoutTests over to be WPTs > > Moving as many of the WebXR LayoutTests as possible over to be > WebPlatformTests. All the tests that have been moved test some > aspect of the spec (https://immersive-web.github.io/webxr/) in > a platform agnostic way. > > Bug: 863557, 844772 > Change-Id: I203b4c59be0daa0ca624fb0281fc33035f1be8a1 > Reviewed-on: https://chromium-review.googlesource.com/1171857 > Reviewed-by: Brandon Jones <bajones@chromium.org> > Reviewed-by: Brian Sheedy <bsheedy@chromium.org> > Commit-Queue: Anna Offenwanger <offenwanger@chromium.org> > Cr-Commit-Position: refs/heads/master@{#585064} Bug: 863557, 844772 Change-Id: I2a85e45018fc0a17fe91c3d7bddd89587a2617e0 Reviewed-on: https://chromium-review.googlesource.com/1185384 Reviewed-by: Brian Sheedy <bsheedy@chromium.org> Commit-Queue: Anna Offenwanger <offenwanger@chromium.org> Cr-Commit-Position: refs/heads/master@{#585342} -- wpt-commits: 50d6ee076e94273080d9f3b69be0bf4eeae156d3 wpt-pr: 12629
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/resources/chromium/webxr-test.js
testing/web-platform/tests/webxr/navigator_xr_requestDevice.https.html
testing/web-platform/tests/webxr/navigator_xr_requestDevice_no_device.https.html
testing/web-platform/tests/webxr/resources/webxr_util.js
testing/web-platform/tests/webxr/webGLCanvasContext_create_with_xrdevice.https.html
testing/web-platform/tests/webxr/webGLCanvasContext_setdevice_contextlost.https.html
testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html
testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_no_gesture.https.html
testing/web-platform/tests/webxr/xrDevice_requestSession_immersive_unsupported.https.html
testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html
testing/web-platform/tests/webxr/xrDevice_supportsSession_immersive.https.html
testing/web-platform/tests/webxr/xrDevice_supportsSession_immersive_unsupported.https.html
testing/web-platform/tests/webxr/xrDevice_supportsSession_non_immersive.https.html
testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html
testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html
testing/web-platform/tests/webxr/xrSession_device.https.html
testing/web-platform/tests/webxr/xrSession_end.https.html
testing/web-platform/tests/webxr/xrSession_exclusive_requestAnimationFrame.https.html
testing/web-platform/tests/webxr/xrSession_prevent_multiple_exclusive.https.html
testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.https.html
testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html
testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getDevicePose.https.html
testing/web-platform/tests/webxr/xrSession_requestFrameOfReference.https.html
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -400703,25 +400703,139 @@
     ]
    ],
    "webxr/idlharness.https.window.js": [
     [
      "/webxr/idlharness.https.window.html",
      {}
     ]
    ],
+   "webxr/navigator_xr_requestDevice.https.html": [
+    [
+     "/webxr/navigator_xr_requestDevice.https.html",
+     {}
+    ]
+   ],
+   "webxr/navigator_xr_requestDevice_no_device.https.html": [
+    [
+     "/webxr/navigator_xr_requestDevice_no_device.https.html",
+     {}
+    ]
+   ],
+   "webxr/webGLCanvasContext_create_with_xrdevice.https.html": [
+    [
+     "/webxr/webGLCanvasContext_create_with_xrdevice.https.html",
+     {}
+    ]
+   ],
+   "webxr/webGLCanvasContext_setdevice_contextlost.https.html": [
+    [
+     "/webxr/webGLCanvasContext_setdevice_contextlost.https.html",
+     {}
+    ]
+   ],
    "webxr/webxr_availability.http.sub.html": [
     [
      "/webxr/webxr_availability.http.sub.html",
      {}
     ]
    ],
-   "webxr/xrSession_exclusive_requestAnimationFrame.https.html": [
-    [
-     "/webxr/xrSession_exclusive_requestAnimationFrame.https.html",
+   "webxr/xrDevice_requestSession_immersive.https.html": [
+    [
+     "/webxr/xrDevice_requestSession_immersive.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_requestSession_immersive_no_gesture.https.html": [
+    [
+     "/webxr/xrDevice_requestSession_immersive_no_gesture.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_requestSession_immersive_unsupported.https.html": [
+    [
+     "/webxr/xrDevice_requestSession_immersive_unsupported.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html": [
+    [
+     "/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_supportsSession_immersive.https.html": [
+    [
+     "/webxr/xrDevice_supportsSession_immersive.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_supportsSession_immersive_unsupported.https.html": [
+    [
+     "/webxr/xrDevice_supportsSession_immersive_unsupported.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrDevice_supportsSession_non_immersive.https.html": [
+    [
+     "/webxr/xrDevice_supportsSession_non_immersive.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_cancelAnimationFrame.https.html": [
+    [
+     "/webxr/xrSession_cancelAnimationFrame.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html": [
+    [
+     "/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_device.https.html": [
+    [
+     "/webxr/xrSession_device.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_end.https.html": [
+    [
+     "/webxr/xrSession_end.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_prevent_multiple_exclusive.https.html": [
+    [
+     "/webxr/xrSession_prevent_multiple_exclusive.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_requestAnimationFrame_callback_calls.https.html": [
+    [
+     "/webxr/xrSession_requestAnimationFrame_callback_calls.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_requestAnimationFrame_data_valid.https.html": [
+    [
+     "/webxr/xrSession_requestAnimationFrame_data_valid.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_requestAnimationFrame_getDevicePose.https.html": [
+    [
+     "/webxr/xrSession_requestAnimationFrame_getDevicePose.https.html",
+     {}
+    ]
+   ],
+   "webxr/xrSession_requestFrameOfReference.https.html": [
+    [
+     "/webxr/xrSession_requestFrameOfReference.https.html",
      {}
     ]
    ],
    "workers/SharedWorkerPerformanceNow.html": [
     [
      "/workers/SharedWorkerPerformanceNow.html",
      {}
     ]
@@ -628369,17 +628483,17 @@
    "9037a109c91610957091f73752074faa031e497b",
    "support"
   ],
   "resources/chromium/webusb-test.js.headers": [
    "6805c323df5a975231648b830e33ce183c3cbbd3",
    "support"
   ],
   "resources/chromium/webxr-test.js": [
-   "10ed703e01b7efe434333a9c69ac27be1aa0cc08",
+   "e5d172599c171e889c25927190d375fcf142cd65",
    "support"
   ],
   "resources/chromium/webxr-test.js.headers": [
    "6805c323df5a975231648b830e33ce183c3cbbd3",
    "support"
   ],
   "resources/idlharness.js": [
    "379785cd68517fe40d4b28d2dc6e2f8e63ea4af7",
@@ -654696,30 +654810,106 @@
   "webxr/META.yml": [
    "b5e3e65a12cb4124e43c4f24b7fca84910fda119",
    "support"
   ],
   "webxr/idlharness.https.window.js": [
    "3e54e367787cb95dada398790fe23b10174df29f",
    "testharness"
   ],
+  "webxr/navigator_xr_requestDevice.https.html": [
+   "c51dd8db165a14514dcf364c760b8da6a81af0b0",
+   "testharness"
+  ],
+  "webxr/navigator_xr_requestDevice_no_device.https.html": [
+   "3cd149bb0ce0df4b7006f4d99a83d2de09e0f282",
+   "testharness"
+  ],
   "webxr/resources/webxr_check.html": [
    "2d8e5b387dc88588921ccfa49dd14db58009900c",
    "support"
   ],
   "webxr/resources/webxr_util.js": [
-   "eaa788798be7be97cf813fecece648d283130254",
-   "support"
+   "d73cf8d44e902b57d4b79fd8d8f68251be7f88a4",
+   "support"
+  ],
+  "webxr/webGLCanvasContext_create_with_xrdevice.https.html": [
+   "94731ccfbb4937cabe175de5acce4423fbf8de43",
+   "testharness"
+  ],
+  "webxr/webGLCanvasContext_setdevice_contextlost.https.html": [
+   "25f9869b70e27ca563df2860dd01a679e140d221",
+   "testharness"
   ],
   "webxr/webxr_availability.http.sub.html": [
    "515b2ad1a8d015120032ea56c473922a2af73e85",
    "testharness"
   ],
-  "webxr/xrSession_exclusive_requestAnimationFrame.https.html": [
-   "010ab0b127e2dfad9371f4d53b61d4e5a44b7250",
+  "webxr/xrDevice_requestSession_immersive.https.html": [
+   "e9e7b9bc131b12d68fa8a2886967a7395a5b0f99",
+   "testharness"
+  ],
+  "webxr/xrDevice_requestSession_immersive_no_gesture.https.html": [
+   "9f0f5f3d8ae0c4b7cb8794ad41cd1015cd942374",
+   "testharness"
+  ],
+  "webxr/xrDevice_requestSession_immersive_unsupported.https.html": [
+   "e0d2688e997be1c765e3e146ff1eb0db212b976e",
+   "testharness"
+  ],
+  "webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html": [
+   "1634dfe9cba3a6798652613c407ec21b5e3c4d8e",
+   "testharness"
+  ],
+  "webxr/xrDevice_supportsSession_immersive.https.html": [
+   "3ca5eb11c53baa319c5188f74c6a0651801e0183",
+   "testharness"
+  ],
+  "webxr/xrDevice_supportsSession_immersive_unsupported.https.html": [
+   "0c1dd25869fcad3a442cde8e849365f0f326b3c9",
+   "testharness"
+  ],
+  "webxr/xrDevice_supportsSession_non_immersive.https.html": [
+   "535b0bcbbc282ee5aaa7500cbd3a35ba21fed3d4",
+   "testharness"
+  ],
+  "webxr/xrSession_cancelAnimationFrame.https.html": [
+   "26c0e95605468634c9bc65bf959260e594998a2d",
+   "testharness"
+  ],
+  "webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html": [
+   "4f4b8dfe5ff47b4433f0de0d244b12dd434eaa0d",
+   "testharness"
+  ],
+  "webxr/xrSession_device.https.html": [
+   "0b482f55a880bc268e7078b419c90b2d617113d8",
+   "testharness"
+  ],
+  "webxr/xrSession_end.https.html": [
+   "69bbda85fda9f732a0d682cf57be53115eb73854",
+   "testharness"
+  ],
+  "webxr/xrSession_prevent_multiple_exclusive.https.html": [
+   "ff3eaef81dd19d945a8c7128c7b5e677ea450689",
+   "testharness"
+  ],
+  "webxr/xrSession_requestAnimationFrame_callback_calls.https.html": [
+   "48dc88779abcdd37a3778f860473569f7c55c6de",
+   "testharness"
+  ],
+  "webxr/xrSession_requestAnimationFrame_data_valid.https.html": [
+   "702932ae62cedef340ccbcfd068dc993a40b521b",
+   "testharness"
+  ],
+  "webxr/xrSession_requestAnimationFrame_getDevicePose.https.html": [
+   "195e653ccfc59aaca582d223072001dc1049309a",
+   "testharness"
+  ],
+  "webxr/xrSession_requestFrameOfReference.https.html": [
+   "ea758761e59de144a742019cc386b083639db6c9",
    "testharness"
   ],
   "workers/META.yml": [
    "a7297d3844728b8bb2f7c82a2c4f32d65040a919",
    "support"
   ],
   "workers/README.md": [
    "78cc74371b981de53be670795a0edbc708c78b58",
--- a/testing/web-platform/tests/resources/chromium/webxr-test.js
+++ b/testing/web-platform/tests/resources/chromium/webxr-test.js
@@ -4,17 +4,21 @@
 // https://github.com/immersive-web/webxr-test-api
 
 class ChromeXRTest {
   constructor() {
     this.mockVRService_ = new MockVRService(mojo.frameInterfaces);
   }
 
   simulateDeviceConnection(init_params) {
-    return Promise.resolve(this.mockVRService_.addDevice(init_params));
+    return Promise.resolve(this.mockVRService_.addRuntime(init_params));
+  }
+
+  simulateDeviceDisconnection(device) {
+    this.mockVRService_.removeRuntime(device);
   }
 
   simulateUserActivation(callback) {
     return new Promise(resolve => {
       let button = document.createElement('button');
       button.textContent = 'click to continue test';
       button.style.display = 'block';
       button.style.fontSize = '20px';
@@ -25,54 +29,112 @@ class ChromeXRTest {
       };
       document.body.appendChild(button);
       test_driver.click(button);
     });
   }
 }
 
 // Mocking class definitions
+
+// Mock service implements both the VRService and XRDevice mojo interfaces.
 class MockVRService {
   constructor() {
     this.bindingSet_ = new mojo.BindingSet(device.mojom.VRService);
-    this.devices_ = [];
+    this.runtimes_ = [];
 
     this.interceptor_ =
         new MojoInterfaceInterceptor(device.mojom.VRService.name);
     this.interceptor_.oninterfacerequest = e =>
         this.bindingSet_.addBinding(this, e.handle);
     this.interceptor_.start();
   }
 
   // Test methods
-  addDevice(fakeDeviceInit) {
-    let device = new MockDevice(fakeDeviceInit, this);
-    this.devices_.push(device);
+  addRuntime(fakeDeviceInit) {
+    let runtime = new MockRuntime(fakeDeviceInit, this);
+    this.runtimes_.push(runtime);
 
     if (this.client_) {
       this.client_.onDeviceChanged();
     }
 
-    return device;
+    return runtime;
+  }
+
+  removeRuntime(runtime) {
+    // We have no way of distinguishing between devices, so just clear the
+    // entire list for now.
+    // TODO(http://crbug.com/873409) We also have no way right now to disconnect
+    // devices.
+    this.runtimes_ = [];
   }
 
   // VRService implementation.
   requestDevice() {
-    return Promise.resolve(
-        {device: this.devices_[0] ? this.devices_[0].getDevicePtr() : null});
+    if (this.runtimes_.length > 0) {
+      let devicePtr = new device.mojom.XRDevicePtr();
+      new mojo.Binding(
+          device.mojom.XRDevice, this, mojo.makeRequest(devicePtr));
+
+      return Promise.resolve({device: devicePtr});
+    } else {
+      return Promise.resolve({device: null});
+    }
   }
 
   setClient(client) {
     this.client_ = client;
   }
+
+  // XRDevice implementation.
+  requestSession(sessionOptions, was_activation) {
+    let requests = [];
+    // Request a session from all the runtimes.
+    for (let i = 0; i < this.runtimes_.length; i++) {
+      requests[i] = this.runtimes_[i].requestRuntimeSession(sessionOptions);
+    }
+
+    return Promise.all(requests).then((results) => {
+      // Find and return the first successful result.
+      for (let i = 0; i < results.length; i++) {
+        if (results[i].session) {
+          return results[i];
+        }
+      }
+
+      // If there were no successful results, returns a null session.
+      return {session: null};
+    });
+  }
+
+  supportsSession(sessionOptions) {
+    let requests = [];
+    // Check supports on all the runtimes.
+    for (let i = 0; i < this.runtimes_.length; i++) {
+      requests[i] = this.runtimes_[i].runtimeSupportsSession(sessionOptions);
+    }
+
+    return Promise.all(requests).then((results) => {
+      // Find and return the first successful result.
+      for (let i = 0; i < results.length; i++) {
+        if (results[i].supportsSession) {
+          return results[i];
+        }
+      }
+
+      // If there were no successful results, returns false.
+      return {supportsSession: false};
+    });
+  };
 }
 
-// Implements both XRDevice and VRMagicWindowProvider. Maintains a mock for
-// XRPresentationProvider.
-class MockDevice {
+// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock
+// for XRPresentationProvider.
+class MockRuntime {
   constructor(fakeDeviceInit, service) {
     this.sessionClient_ = new device.mojom.XRSessionClientPtr();
     this.presentation_provider_ = new MockXRPresentationProvider();
 
     this.pose_ = null;
     this.next_frame_id_ = 0;
 
     this.service_ = service;
@@ -81,23 +143,16 @@ class MockDevice {
 
     if (fakeDeviceInit.supportsImmersive) {
       this.displayInfo_ = this.getImmersiveDisplayInfo();
     } else {
       this.displayInfo_ = this.getNonImmersiveDisplayInfo();
     }
   }
 
-  // Functions for setup.
-  getDevicePtr() {
-    let devicePtr = new device.mojom.XRDevicePtr();
-    new mojo.Binding(device.mojom.XRDevice, this, mojo.makeRequest(devicePtr));
-    return devicePtr;
-  }
-
   // Test methods.
   setXRPresentationFrameData(poseMatrix, views) {
     if (poseMatrix == null) {
       this.pose_ = null;
     } else {
       this.setPoseFromMatrix(poseMatrix);
     }
 
@@ -268,19 +323,20 @@ class MockDevice {
     });
   }
 
   updateSessionGeometry(frame_size, display_rotation) {
     // This function must exist to ensure that calls to it do not crash, but we
     // do not have any use for this data at present.
   }
 
-  // XRDevice implementation.
-  requestSession(sessionOptions, was_activation) {
-    return this.supportsSession(sessionOptions).then((result) => {
+
+  // Utility function
+  requestRuntimeSession(sessionOptions) {
+    return this.runtimeSupportsSession(sessionOptions).then((result) => {
       // The JavaScript bindings convert c_style_names to camelCase names.
       let options = new device.mojom.XRPresentationTransportOptions();
       options.transportMethod =
           device.mojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER;
       options.waitForTransferNotification = true;
       options.waitForRenderNotification = true;
 
       let submit_frame_sink;
@@ -315,17 +371,17 @@ class MockDevice {
           }
         });
       } else {
         return Promise.resolve({session: null});
       }
     });
   }
 
-  supportsSession(options) {
+  runtimeSupportsSession(options) {
     return Promise.resolve({
       supportsSession:
           !options.immersive || this.displayInfo_.capabilities.canPresent
     });
   };
 }
 
 class MockXRPresentationProvider {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/navigator_xr_requestDevice.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+
+  <script>
+    xr_promise_test("navigator.xr.requestDevice returns a device", (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive: true })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => {
+          t.step(() => {
+            assert_true(device != null);
+            assert_true(device instanceof XRDevice);
+          });
+        });
+    });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/navigator_xr_requestDevice_no_device.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+
+  <script>
+    promise_test( (t) => {
+      return promise_rejects(t, 'NotFoundError', navigator.xr.requestDevice());
+    }, "navigator.xr.requestDevice properly rejects when there are 0 devices");
+  </script>
+</body>
--- a/testing/web-platform/tests/webxr/resources/webxr_util.js
+++ b/testing/web-platform/tests/webxr/resources/webxr_util.js
@@ -2,29 +2,97 @@
 // 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
 
-function xr_promise_test(func, name, properties) {
+function xr_promise_test(name, func, properties) {
   promise_test(async (t) => {
     // Perform any required test setup:
 
     if (window.XRTest === undefined) {
       // Chrome setup
       await loadChromiumResources;
     }
 
     return func(t);
   }, name, properties);
 }
 
+// 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.
+// Requires a webglCanvas on the page.
+function xr_session_promise_test(
+    name, func, fakeDeviceInit, sessionOptions, properties) {
+  let testDevice;
+  let testDeviceController;
+  let testSession;
+
+  const webglCanvas = document.getElementsByTagName('canvas')[0];
+  if (!webglCanvas) {
+    promise_test(async (t) => {
+      Promise.reject('xr_session_promise_test requires a canvas on the page!');
+    }, name, properties);
+  }
+  let gl = webglCanvas.getContext('webgl', {alpha: false, antialias: false});
+
+  xr_promise_test(
+      name,
+      (t) =>
+          XRTest.simulateDeviceConnection(fakeDeviceInit)
+              .then((controller) => {
+                testDeviceController = controller;
+                return navigator.xr.requestDevice();
+              })
+              .then((device) => {
+                testDevice = device;
+                return gl.setCompatibleXRDevice(device);
+              })
+              .then(() => new Promise((resolve, reject) => {
+                      // Perform the session request in a user gesture.
+                      XRTest.simulateUserActivation(() => {
+                        testDevice.requestSession(sessionOptions)
+                            .then((session) => {
+                              testSession = session;
+                              // Session must have a baseLayer or frame requests
+                              // will be ignored.
+                              session.baseLayer = new XRWebGLLayer(session, gl);
+                              resolve(func(session, testDeviceController, t));
+                            })
+                            .catch((err) => {
+                              reject(
+                                  'Session with params ' +
+                                  JSON.stringify(sessionOptions) +
+                                  ' was rejected on device ' +
+                                  JSON.stringify(fakeDeviceInit) +
+                                  ' with error: ' + err);
+                            });
+                      });
+                    }))
+              .then(() => {
+                // Cleanup system state.
+                testSession.end().catch(() => {});
+                XRTest.simulateDeviceDisconnection(testDevice);
+              }),
+      properties);
+}
+
+// A utility function to create an output context as required by non-immersive
+// sessions.
+// https://immersive-web.github.io/webxr/#xrsessioncreationoptions-interface
+function getOutputContext() {
+  let outputCanvas = document.createElement('canvas');
+  document.body.appendChild(outputCanvas);
+  return outputCanvas.getContext('xrpresent');
+}
+
 // 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) {
@@ -54,18 +122,23 @@ let loadChromiumResources = Promise.reso
   if (!MojoInterfaceInterceptor) {
     // Do nothing on non-Chromium-based browsers or when the Mojo bindings are
     // not present in the global namespace.
     return;
   }
 
   let chain = Promise.resolve();
   ['/gen/layout_test_data/mojo/public/js/mojo_bindings.js',
+   '/gen/mojo/public/mojom/base/time.mojom.js',
+   '/gen/gpu/ipc/common/mailbox_holder.mojom.js',
+   '/gen/gpu/ipc/common/sync_token.mojom.js',
+   '/gen/ui/display/mojo/display.mojom.js',
    '/gen/ui/gfx/geometry/mojo/geometry.mojom.js',
-   '/gen/mojo/public/mojom/base/time.mojom.js',
+   '/gen/ui/gfx/mojo/gpu_fence_handle.mojom.js',
+   '/gen/ui/gfx/mojo/transform.mojom.js',
    '/gen/device/vr/public/mojom/vr_service.mojom.js',
    '/resources/chromium/webxr-test.js', '/resources/testdriver.js',
    '/resources/testdriver-vendor.js',
   ].forEach(path => {
     let script = document.createElement('script');
     script.src = path;
     script.async = false;
     chain = chain.then(() => new Promise(resolve => {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/webGLCanvasContext_create_with_xrdevice.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>
+  <canvas id="webgl-canvas"></canvas>
+  <script>
+    xr_promise_test("webglCanvasContext can be created with an XRDevice",
+      (t) => {
+        return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+          .then( (controller) => { return navigator.xr.requestDevice() })
+          .then( (device) => {
+            webglCanvas = document.getElementById('webgl-canvas');
+            gl = webglCanvas.getContext('webgl', {compatibleXRDevice: device});
+            assert_equals(gl.getContextAttributes().compatibleXRDevice, device);
+
+            // Check that an offscreen context behaves no different.
+            let offscreenCanvas = document.createElement('canvas');
+            let offscreenGl = webglCanvas.getContext(
+              'webgl', {compatibleXRDevice: device});
+            assert_equals(
+              offscreenGl.getContextAttributes().compatibleXRDevice, device);
+          });
+      });
+
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/webGLCanvasContext_setdevice_contextlost.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas id="webgl-canvas"></canvas>
+  <script>
+
+    xr_promise_test(
+      "A lost webglCanvasContext should not be able to set device",
+     (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => {
+          webglCanvas = document.getElementById('webgl-canvas');
+          gl = webglCanvas.getContext('webgl', {compatibleXRDevice: device});
+          gl.getExtension('WEBGL_lose_context').loseContext();
+          return promise_rejects(t, 'InvalidStateError',
+              gl.setCompatibleXRDevice(device));
+        });
+    });
+
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_immersive.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+  <script>
+    xr_session_promise_test(
+      "Tests requestSession resolves when supported",
+      (session) => {
+        assert_not_equals(session, null);
+      }, { supportsImmersive:true }, { immersive: true });
+  </script>
+</body>
\ No newline at end of file
new file mode 100644
--- /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>
+    xr_promise_test(
+      "Requesting immersive session outside of a user gesture rejects",
+      (t) => {
+        return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+          .then( (controller) => { return navigator.xr.requestDevice() })
+          .then( (device) => promise_rejects(
+            t, 'SecurityError', device.requestSession({ immersive: true })));
+      });
+  </script>
+</body>
new file mode 100644
--- /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>
+    xr_promise_test(
+      "Requesting an immersive session when unsupported rejects",
+      (t) => {
+        return XRTest.simulateDeviceConnection({ supportsImmersive:false })
+          .then( (controller) => { return navigator.xr.requestDevice() })
+          .then( (magicWindowOnlyDevice) => new Promise((resolve) => {
+            XRTest.simulateUserActivation( () => {
+              resolve(promise_rejects(
+                t,
+                "NotSupportedError",
+                magicWindowOnlyDevice.requestSession({ immersive: true })
+              ))
+            });
+          }));
+      });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrDevice_requestSession_non_immersive_no_gesture.https.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <script>
+    xr_promise_test(
+      "Requesting non-immersive session outside of a user gesture succeeds",
+      (t) => {
+        return XRTest.simulateDeviceConnection({ supportsImmersive:false })
+          .then( (controller) => { return navigator.xr.requestDevice(); })
+          .then( (device) => device.requestSession({
+            immersive: false,
+            outputContext: getOutputContext()
+          }))
+          .then( (session) => { assert_not_equals(session, null); });
+      });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrDevice_supportsSession_immersive.https.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <script>
+    xr_promise_test(
+      "supportsSession resolves when immersive options supported",
+      () => {
+        return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+          .then( (controller) => { return navigator.xr.requestDevice() })
+          .then( (device) => device.supportsSession({ immersive: true }));
+      });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrDevice_supportsSession_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>
+    xr_promise_test(
+      "supportsSession rejects when options not supported",
+      (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive:false })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => {
+          return promise_rejects(
+            t,
+            "NotSupportedError",
+            device.supportsSession({ immersive: true })
+          );
+        });
+    });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrDevice_supportsSession_non_immersive.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>
+    xr_promise_test(
+      "supportsSession resolves when non-immersive options supported",
+      (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => {
+          // Non-immersive sessions without a outputContext should not be supported.
+          promise_rejects(t, 'NotSupportedError', device.supportsSession());
+
+          // Non-immersive sessions with an outputContext should be supported.
+          return device.supportsSession({
+              outputContext: getOutputContext()
+          });
+        });
+    });
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame.https.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+
+  <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 = { supportsImmersive:true };
+
+    let immersiveSessionOptions = { immersive: true };
+    let nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    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, immersiveSessionOptions);
+    xr_session_promise_test(nonImmersiveTestName, testFunction,
+      fakeDeviceInitParams, nonImmersiveSessionOptions);
+
+  </script>
+</body>
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_cancelAnimationFrame_invalidhandle.https.html
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+  <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 = { supportsImmersive:true };
+
+    let immersiveSessionOptions = { immersive: true };
+    let nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    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, immersiveSessionOptions);
+    xr_session_promise_test(
+      nonImmersiveTestName, testFunction, fakeDeviceInitParams, nonImmersiveSessionOptions);
+
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_device.https.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+
+  <script>
+    xr_promise_test("Requested session has device set",
+     (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => new Promise((resolve) => {
+          XRTest.simulateUserActivation( () => {
+            resolve(device.requestSession({ immersive: true }).then( (session) => {
+              assert_true(session.immersive);
+              assert_equals(session.device, device);
+            }));
+          });
+        }));
+    });
+  </script>
+</body>
+
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_end.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>
+  <canvas></canvas>
+
+  <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 = { supportsImmersive:true };
+
+    const immersiveSessionOptions = { immersive: true };
+    const nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    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);
+          session.dispatchEvent(watcherDone);
+        });
+      }
+      session.addEventListener("end", onSessionEnd, false);
+      session.end();
+
+      return eventPromise;
+    };
+
+    xr_session_promise_test(immersivetestName, testFunction,
+      fakeDeviceInitParams, immersiveSessionOptions);
+    xr_session_promise_test(nonimmersiveTestName, testFunction,
+      fakeDeviceInitParams, nonImmersiveSessionOptions);
+  </script>
+</body>
\ No newline at end of file
deleted file mode 100644
--- a/testing/web-platform/tests/webxr/xrSession_exclusive_requestAnimationFrame.https.html
+++ /dev/null
@@ -1,91 +0,0 @@
-<!DOCTYPE html>
-<body>
-  <script src=/resources/testharness.js></script>
-  <script src=/resources/testharnessreport.js></script>
-  <script src="resources/webxr_util.js"></script>
-  <canvas id="webgl-canvas"></canvas>
-
-  <script>
-
-  const identityMatrix = new Float32Array([
-    1, 0, 0, 0,
-    0, 1, 0, 0,
-    0, 0, 1, 0,
-    0, 0, 0, 1
-  ]);
-
-  const rightFakeXRViewInit =
-    {eye:"right", projectionMatrix: identityMatrix, viewMatrix: identityMatrix};
-
-  const leftFakeXRViewInit =
-    {eye:"left", projectionMatrix: identityMatrix, viewMatrix: identityMatrix};
-
-  const immersiveFakeXRDeviceInit = { supportsImmersive:true };
-
-  const webglCanvas = document.getElementById('webgl-canvas');
-  let gl = webglCanvas.getContext('webgl', { alpha: false, antialias: false });
-
-  let testDevice;
-  let testDeviceController;
-  let testSession;
-
-  xr_promise_test(
-    (t) => XRTest.simulateDeviceConnection(immersiveFakeXRDeviceInit)
-      .then((controller) => {
-        testDeviceController = controller;
-        return navigator.xr.requestDevice();
-      })
-      .then((device) => {
-        testDevice = device;
-        return gl.setCompatibleXRDevice(device);
-      })
-      .then(() => new Promise((resolve, reject) => {
-          // Perform the session request in a user gesture.
-          XRTest.simulateUserActivation(() => {
-            testDevice.requestSession({ immersive: true })
-              .then((session) => {
-                testSession = session;
-                return session.requestFrameOfReference('eye-level');
-              })
-              .then((frameOfRef) => {
-                // Session must have a baseLayer or frame requests will be ignored.
-                testSession.baseLayer = new XRWebGLLayer(testSession, gl);
-
-                function onFrame(time, xrFrame) {
-                  assert_true(xrFrame instanceof XRFrame);
-
-                  assert_not_equals(xrFrame.views, null);
-                  assert_equals(xrFrame.views.length, 2);
-
-                  let devicePose = xrFrame.getDevicePose(frameOfRef);
-
-                  assert_not_equals(devicePose, null);
-                  for(let i = 0; i < identityMatrix.length; i++) {
-                    assert_equals(devicePose.poseModelMatrix[i], identityMatrix[i]);
-                  }
-
-                  assert_not_equals(devicePose.getViewMatrix(xrFrame.views[0]), null);
-                  assert_equals(devicePose.getViewMatrix(xrFrame.views[0]).length, 16);
-                  assert_not_equals(devicePose.getViewMatrix(xrFrame.views[1]), null);
-                  assert_equals(devicePose.getViewMatrix(xrFrame.views[1]).length, 16);
-
-                  // Test does not complete until the returned promise resolves.
-                  resolve();
-                }
-
-                testDeviceController.setXRPresentationFrameData(
-                  identityMatrix,
-                  [rightFakeXRViewInit, leftFakeXRViewInit]
-                );
-
-                testSession.requestAnimationFrame(onFrame);
-              }).catch((err) => {
-                reject("Session was rejected with error: "+err);
-              });
-          });
-        })
-      ),
-    "RequestAnimationFrame resolves with good data"
-  );
-  </script>
-</body>
new file mode 100644
--- /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>
+  <canvas></canvas>
+
+  <script>
+    xr_promise_test(
+      "Test prevention of multiple simultaneous immersive sessions",
+      (t) => {
+      return XRTest.simulateDeviceConnection({ supportsImmersive:true })
+        .then( (controller) => { return navigator.xr.requestDevice() })
+        .then( (device) => new Promise((resolve) => {
+          XRTest.simulateUserActivation( () => {
+            resolve(device.requestSession({ immersive: true })
+              .then( (session) => new Promise((resolve) => {
+                XRTest.simulateUserActivation( () => {
+                  // Requesting a second immersive session from a device that already
+                  // has an active immersive session 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(
+                    t,
+                    "InvalidStateError",
+                    device.requestSession({ immersive: true })
+                  ).then( () => {
+                      // End the immersive session and try again. Now the immersive
+                      // session creation should succeed.
+                      return session.end().then( () => new Promise((resolve) => {
+                        XRTest.simulateUserActivation( () => {
+                          resolve(device.requestSession({ immersive: true }));
+                        });
+                      }));
+                    }));
+                });
+            })));
+          });
+        }));
+    });
+
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_callback_calls.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>
+  <canvas></canvas>
+
+  <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 = { supportsImmersive:true };
+
+    let immersiveSessionOptions = { immersive: true };
+    let nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    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, immersiveSessionOptions);
+    xr_session_promise_test(nonImmersiveTestName, testFunction,
+      fakeDeviceInitParams, nonImmersiveSessionOptions);
+
+  </script>
+</body>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_data_valid.https.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+
+  <script>
+    const testName = "RequestAnimationFrame resolves with good data";
+
+    const identityMatrix = new Float32Array([
+      1, 0, 0, 0,
+      0, 1, 0, 0,
+      0, 0, 1, 0,
+      0, 0, 0, 1
+    ]);
+
+    const rightFakeXRViewInit = {
+      eye:"right",
+      projectionMatrix: identityMatrix,
+      viewMatrix: identityMatrix
+    };
+
+    const leftFakeXRViewInit = {
+      eye:"left",
+      projectionMatrix: identityMatrix,
+      viewMatrix: identityMatrix
+    };
+
+    const fakeDeviceInitOptions = { supportsImmersive:true };
+    const sessionOptions = { immersive:true };
+
+    let testSession;
+
+    let testFunction = function(session, testDeviceController) {
+      testSession = session;
+      return session.requestFrameOfReference('eye-level')
+        .then((frameOfRef) => new Promise((resolve) => {
+
+          function onFrame(time, xrFrame) {
+            assert_true(xrFrame instanceof XRFrame);
+
+            assert_not_equals(xrFrame.views, null);
+            assert_equals(xrFrame.views.length, 2);
+
+            let devicePose = xrFrame.getDevicePose(frameOfRef);
+
+            assert_not_equals(devicePose, null);
+            for(let i = 0; i < identityMatrix.length; i++) {
+              assert_equals(devicePose.poseModelMatrix[i], identityMatrix[i]);
+            }
+
+            assert_not_equals(devicePose.getViewMatrix(xrFrame.views[0]), null);
+            assert_equals(devicePose.getViewMatrix(xrFrame.views[0]).length, 16);
+            assert_not_equals(devicePose.getViewMatrix(xrFrame.views[1]), null);
+            assert_equals(devicePose.getViewMatrix(xrFrame.views[1]).length, 16);
+
+            // Test does not complete until the returned promise resolves.
+            resolve();
+          }
+
+          testDeviceController.setXRPresentationFrameData(
+            identityMatrix,
+            [rightFakeXRViewInit, leftFakeXRViewInit]
+          );
+
+          testSession.requestAnimationFrame(onFrame);
+        })
+      );
+    }
+
+    xr_session_promise_test(
+      testName, testFunction, fakeDeviceInitOptions, sessionOptions);
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_requestAnimationFrame_getDevicePose.https.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+
+  <script>
+
+    let immersiveTestName =
+      "XRFrame getDevicePose updates on the next frame for immersive sessions";
+    let nonImmersiveTestName =
+      "XRFrame getDevicePose updates on the next frame for non-immersive sessions";
+
+    let fakeDeviceInitParams = { supportsImmersive: true };
+
+    let immersiveSessionOptions = { immersive: true };
+    let nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    // Valid matrices for  when we don't care about specific values
+    const validPoseMatrix = [0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1];
+    const validProjectionMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 3, 2, -1, -1, 0, 0, -0.2, 0];
+    const validViewMatrix = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4, 3, 2, 1];
+
+    let testFunction = function(session, fakeDeviceController, t) {
+      return session.requestFrameOfReference("eye-level")
+        .then((frameOfRef) => new Promise((resolve, reject) => {
+          let counter = 0;
+          function onFrame(time, vrFrame) {
+            session.requestAnimationFrame(onFrame);
+            if (counter == 0) {
+              t.step( () => {
+                // Expecting to not get a pose since none has been supplied
+                assert_equals(vrFrame.getDevicePose(frameOfRef), null);
+
+                fakeDeviceController.setXRPresentationFrameData(
+                  validPoseMatrix, [{
+                    eye:"left",
+                    projectionMatrix: validProjectionMatrix,
+                    viewMatrix: validViewMatrix
+                  }, {
+                    eye:"right",
+                    projectionMatrix: validProjectionMatrix,
+                    viewMatrix: validViewMatrix
+                  }]);
+
+                // Check that pose does not update pose within the same frame.
+                assert_equals(vrFrame.getDevicePose(frameOfRef), null);
+              });
+            } else {
+              t.step( () => {
+                let pose = vrFrame.getDevicePose(frameOfRef);
+                assert_not_equals(pose, null);
+
+                let poseMatrix = pose.poseModelMatrix;
+                assert_not_equals(poseMatrix, null);
+
+                for(let i = 0; i < poseMatrix.length; i++) {
+                  assert_equals(poseMatrix[i], validPoseMatrix[i]);
+                }
+              });
+
+              // Finished.
+              resolve();
+            }
+            counter++;
+          }
+
+          session.requestAnimationFrame(onFrame);
+        }));
+    };
+
+    xr_session_promise_test(nonImmersiveTestName, testFunction,
+      fakeDeviceInitParams, nonImmersiveSessionOptions);
+    xr_session_promise_test(immersiveTestName, testFunction,
+      fakeDeviceInitParams, immersiveSessionOptions);
+
+  </script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/webxr/xrSession_requestFrameOfReference.https.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<body>
+  <script src=/resources/testharness.js></script>
+  <script src=/resources/testharnessreport.js></script>
+  <script src="resources/webxr_util.js"></script>
+  <canvas></canvas>
+  <script>
+
+    let immersiveTestName =
+      "Immersive XRSession requestFrameOfReference returns expected objects";
+    let nonImmersiveTestName =
+      "Non-immersive XRSession requestFrameOfReference returns expected objects";
+
+    let fakeDeviceInitParams = { supportsImmersive: true };
+
+    let immersiveSessionOptions = { immersive: true };
+    let nonImmersiveSessionOptions = { outputContext: getOutputContext() };
+
+    let testFunction = function(session, fakeDeviceController, t) {
+      return promise_rejects(t, new TypeError(), session.requestFrameOfReference("foo"))
+        .then(() => Promise.all([
+          session.requestFrameOfReference("head-model").then( (frameOfRef) => {
+            assert_true(frameOfRef instanceof XRCoordinateSystem,
+              "head-model frameOfRef is not correct type.");
+            assert_true(frameOfRef instanceof XRFrameOfReference,
+              "head-model frameOfRef is not correct type.");
+          }),
+          session.requestFrameOfReference("eye-level").then( (frameOfRef) => {
+            assert_true(frameOfRef instanceof XRCoordinateSystem,
+              "eye-level frameOfRef is not correct type.");
+            assert_true(frameOfRef instanceof XRFrameOfReference,
+              "eye-level frameOfRef is not correct type.");
+          }),
+          session.requestFrameOfReference("stage").then( (frameOfRef) => {
+            assert_true(frameOfRef instanceof XRCoordinateSystem,
+              "stage frameOfRef is not correct type.");
+            assert_true(frameOfRef instanceof XRFrameOfReference,
+              "stage frameOfRef is not correct type.");
+          })
+      ]));
+    };
+
+    xr_session_promise_test(
+      immersiveTestName, testFunction, fakeDeviceInitParams, immersiveSessionOptions);
+    xr_session_promise_test(
+      nonImmersiveTestName, testFunction, fakeDeviceInitParams, nonImmersiveSessionOptions);
+
+  </script>
+</body>
\ No newline at end of file