blob: dbe54d2be0bff240a5be981374c343c460a306d2 [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/vr/test/mock_xr_device_hook_base.h"
#include "chrome/browser/vr/test/webvr_browser_test.h"
#include "chrome/browser/vr/test/webxr_vr_browser_test.h"
#include "device/vr/public/mojom/browser_test_interfaces.mojom.h"
#include "third_party/openvr/src/headers/openvr.h"
// Browser test equivalent of
// chrome/android/javatests/src/.../browser/vr/WebXrVrInputTest.java.
// End-to-end tests for user input interaction with WebXR/WebVR.
namespace vr {
// Test that focus is locked to the presenting display for the purposes of VR/XR
// input.
void TestPresentationLocksFocusImpl(WebXrVrBrowserTestBase* t,
std::string filename) {
t->LoadUrlAndAwaitInitialization(t->GetFileUrlForHtmlTestFile(filename));
t->EnterSessionWithUserGestureOrFail();
t->ExecuteStepAndWait("stepSetupFocusLoss()");
t->EndTest();
}
IN_PROC_BROWSER_TEST_F(WebVrBrowserTestStandard, TestPresentationLocksFocus) {
TestPresentationLocksFocusImpl(this, "test_presentation_locks_focus");
}
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestPresentationLocksFocus) {
TestPresentationLocksFocusImpl(this, "webxr_test_presentation_locks_focus");
}
class WebXrControllerInputMock : public MockXRDeviceHookBase {
public:
void OnFrameSubmitted(
device_test::mojom::SubmittedFrameDataPtr frame_data,
device_test::mojom::XRTestHook::OnFrameSubmittedCallback callback) final;
void WaitNumFrames(unsigned int num_frames) {
DCHECK(!wait_loop_);
target_submitted_frames_ = num_submitted_frames_ + num_frames;
wait_loop_ = new base::RunLoop(base::RunLoop::Type::kNestableTasksAllowed);
wait_loop_->Run();
delete wait_loop_;
wait_loop_ = nullptr;
}
// TODO(https://crbug.com/887726): Figure out why waiting for OpenVR to grab
// the updated state instead of waiting for a number of frames causes frames
// to be submitted at an extremely slow rate. Once fixed, switch away from
// waiting on number of frames.
void UpdateControllerAndWait(
unsigned int index,
const device::ControllerFrameData& controller_data) {
UpdateController(index, controller_data);
WaitNumFrames(30);
}
void ToggleButtonTouches(unsigned int index, uint64_t button_mask) {
auto controller_data = GetCurrentControllerData(index);
controller_data.packet_number++;
controller_data.buttons_touched ^= button_mask;
UpdateControllerAndWait(index, controller_data);
}
void ToggleButtons(unsigned int index, uint64_t button_mask) {
auto controller_data = GetCurrentControllerData(index);
controller_data.packet_number++;
controller_data.buttons_pressed ^= button_mask;
controller_data.buttons_touched ^= button_mask;
UpdateControllerAndWait(index, controller_data);
}
void ToggleTriggerButton(unsigned int index, vr::EVRButtonId button_id) {
auto controller_data = GetCurrentControllerData(index);
uint64_t button_mask = vr::ButtonMaskFromId(button_id);
controller_data.packet_number++;
controller_data.buttons_pressed ^= button_mask;
controller_data.buttons_touched ^= button_mask;
bool is_pressed = ((controller_data.buttons_pressed & button_mask) != 0);
unsigned int axis_offset = GetAxisOffset(button_id);
DCHECK(controller_data.axis_data[axis_offset].axis_type ==
vr::k_eControllerAxis_Trigger);
controller_data.axis_data[axis_offset].x = is_pressed ? 1.0 : 0.0;
UpdateControllerAndWait(index, controller_data);
}
void SetAxes(unsigned int index,
vr::EVRButtonId button_id,
float x,
float y) {
auto controller_data = GetCurrentControllerData(index);
unsigned int axis_offset = GetAxisOffset(button_id);
DCHECK(controller_data.axis_data[axis_offset].axis_type != 0);
controller_data.packet_number++;
controller_data.axis_data[axis_offset].x = x;
controller_data.axis_data[axis_offset].y = y;
UpdateControllerAndWait(index, controller_data);
}
void TogglePrimaryTrigger(unsigned int index) {
ToggleTriggerButton(index, vr::k_EButton_SteamVR_Trigger);
}
void PressReleasePrimaryTrigger(unsigned int index) {
TogglePrimaryTrigger(index);
TogglePrimaryTrigger(index);
}
unsigned int CreateAndConnectMinimalGamepad() {
// Create a controller that only supports select and one TrackPad, i.e. it
// has just enough data to be considered a gamepad.
uint64_t supported_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger) |
vr::ButtonMaskFromId(vr::k_EButton_Axis0);
std::map<vr::EVRButtonId, unsigned int> axis_types = {
{vr::k_EButton_Axis0, vr::k_eControllerAxis_TrackPad},
{vr::k_EButton_SteamVR_Trigger, vr::k_eControllerAxis_Trigger},
};
return CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
}
unsigned int CreateAndConnectController(
device::ControllerRole role,
std::map<vr::EVRButtonId, unsigned int> axis_types = {},
uint64_t supported_buttons = UINT64_MAX) {
auto controller = CreateValidController(role);
controller.supported_buttons = supported_buttons;
for (const auto& axis_type : axis_types) {
unsigned int axis_offset = GetAxisOffset(axis_type.first);
controller.axis_data[axis_offset].axis_type = axis_type.second;
}
return ConnectController(controller);
}
void UpdateControllerSupport(
unsigned int controller_index,
const std::map<vr::EVRButtonId, unsigned int>& axis_types,
uint64_t supported_buttons) {
auto controller_data = GetCurrentControllerData(controller_index);
for (unsigned int i = 0; i < device::kMaxNumAxes; i++) {
auto button_id = GetAxisId(i);
auto it = axis_types.find(button_id);
unsigned int new_axis_type = k_eControllerAxis_None;
if (it != axis_types.end())
new_axis_type = it->second;
controller_data.axis_data[i].axis_type = new_axis_type;
}
controller_data.supported_buttons = supported_buttons;
UpdateControllerAndWait(controller_index, controller_data);
}
private:
vr::EVRButtonId GetAxisId(unsigned int offset) {
return static_cast<vr::EVRButtonId>(vr::k_EButton_Axis0 + offset);
}
unsigned int GetAxisOffset(vr::EVRButtonId button_id) {
DCHECK(vr::k_EButton_Axis0 <= button_id &&
button_id < (vr::k_EButton_Axis0 + device::kMaxNumAxes));
return static_cast<unsigned int>(button_id) -
static_cast<unsigned int>(vr::k_EButton_Axis0);
}
device::ControllerFrameData GetCurrentControllerData(unsigned int index) {
auto iter = controller_data_map_.find(index);
DCHECK(iter != controller_data_map_.end());
return iter->second;
}
base::RunLoop* wait_loop_ = nullptr;
unsigned int num_submitted_frames_ = 0;
unsigned int target_submitted_frames_ = 0;
};
void WebXrControllerInputMock::OnFrameSubmitted(
device_test::mojom::SubmittedFrameDataPtr frame_data,
device_test::mojom::XRTestHook::OnFrameSubmittedCallback callback) {
num_submitted_frames_++;
if (wait_loop_ && target_submitted_frames_ == num_submitted_frames_) {
wait_loop_->Quit();
}
std::move(callback).Run();
}
// Ensure that changes to a gamepad object respect that it is the same object
// and that if whether or not an input source has a gamepad changes that the
// input source change event is fired and a new input source is created.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestInputGamepadSameObject) {
WebXrControllerInputMock my_mock;
// Create a set of buttons and axes that don't have enough data to be made
// into an xr-standard gamepad (which we expect the runtimes to not report).
// Note that we need to set the trigger axis because of how OpenVR handles
// selects.
uint64_t insufficient_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger);
std::map<vr::EVRButtonId, unsigned int> insufficient_axis_types = {
{vr::k_EButton_SteamVR_Trigger, vr::k_eControllerAxis_Trigger},
};
// Create a set of buttons and axes that we expect to have enough data to be
// made into an xr-standard gamepad (which we expect the runtimes to report).
uint64_t sufficient_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger) |
vr::ButtonMaskFromId(vr::k_EButton_Axis0);
std::map<vr::EVRButtonId, unsigned int> sufficient_axis_types = {
{vr::k_EButton_Axis0, vr::k_eControllerAxis_TrackPad},
{vr::k_EButton_SteamVR_Trigger, vr::k_eControllerAxis_Trigger},
};
// Start off without a gamepad.
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, insufficient_axis_types,
insufficient_buttons);
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_webxr_input_same_object"));
EnterSessionWithUserGestureOrFail();
RunJavaScriptOrFail("setupListeners()");
// We should only have seen the first change indicating we have input sources.
PollJavaScriptBooleanOrFail("inputChangeEvents === 1", kPollTimeoutShort);
// We only expect one input source, cache it.
RunJavaScriptOrFail("validateInputSourceLength(1)");
RunJavaScriptOrFail("updateCachedInputSource(0)");
// Toggle a button and confirm that the controller is still the same.
my_mock.PressReleasePrimaryTrigger(controller_index);
RunJavaScriptOrFail("validateCachedSourcePresence(true)");
RunJavaScriptOrFail("validateCurrentAndCachedGamepadMatch()");
// Update the controller to now support a gamepad and verify that we get a
// change event and that the old controller isn't present. Then cache the new
// one.
my_mock.UpdateControllerSupport(controller_index, sufficient_axis_types,
sufficient_buttons);
PollJavaScriptBooleanOrFail("inputChangeEvents === 2", kPollTimeoutShort);
RunJavaScriptOrFail("validateCachedSourcePresence(false)");
RunJavaScriptOrFail("validateInputSourceLength(1)");
RunJavaScriptOrFail("updateCachedInputSource(0)");
// Toggle a button and confirm that the controller is still the same.
my_mock.PressReleasePrimaryTrigger(controller_index);
RunJavaScriptOrFail("validateCachedSourcePresence(true)");
RunJavaScriptOrFail("validateCurrentAndCachedGamepadMatch()");
// Switch back to the insufficient gamepad and confirm that we get the change.
my_mock.UpdateControllerSupport(controller_index, insufficient_axis_types,
insufficient_buttons);
PollJavaScriptBooleanOrFail("inputChangeEvents === 3", kPollTimeoutShort);
RunJavaScriptOrFail("validateCachedSourcePresence(false)");
RunJavaScriptOrFail("validateInputSourceLength(1)");
RunJavaScriptOrFail("done()");
EndTest();
}
// Ensure that if the controller lacks enough data to be considered a Gamepad
// that the input source that it is associated with does not have a Gamepad.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestGamepadIncompleteData) {
WebXrControllerInputMock my_mock;
// Create a controller that only supports select, i.e. it lacks enough data
// to be considered a gamepad.
uint64_t supported_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger);
my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, {}, supported_buttons);
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_webxr_gamepad_support"));
EnterSessionWithUserGestureOrFail();
ExecuteStepAndWait("validateInputSourceHasNoGamepad()");
RunJavaScriptOrFail("done()");
EndTest();
}
// Ensure that if a Gamepad has the minimum required number of axes/buttons to
// be considered an xr-standard Gamepad, that it is exposed as such, and that
// we can check the state of it's priamry axes/button.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestGamepadMinimumData) {
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_webxr_gamepad_support"));
EnterSessionWithUserGestureOrFail();
// Press the trigger and set the axis to a non-zero amount, so we can ensure
// we aren't getting just default gamepad data.
my_mock.TogglePrimaryTrigger(controller_index);
my_mock.SetAxes(controller_index, vr::k_EButton_Axis0, 0.5, -0.5);
// The trigger should be button 0, and the first set of axes should have it's
// value set.
ExecuteStepAndWait("validateButtonPressed(0)");
ExecuteStepAndWait("validateAxesValues(0, 0.5, -0.5)");
RunJavaScriptOrFail("done()");
EndTest();
}
// Ensure that if a Gamepad has all of the required and optional buttons as
// specified by the xr-standard mapping, that those buttons are plumbed up
// in their required places.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestGamepadCompleteData) {
WebXrControllerInputMock my_mock;
// Create a controller that supports all reserved buttons. Note that per
// third_party/openvr/src/headers/openvr.h, SteamVR_Trigger is Axis1.
uint64_t supported_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger) |
vr::ButtonMaskFromId(vr::k_EButton_Axis0) |
vr::ButtonMaskFromId(vr::k_EButton_Axis2) |
vr::ButtonMaskFromId(vr::k_EButton_Grip);
std::map<vr::EVRButtonId, unsigned int> axis_types = {
{vr::k_EButton_Axis0, vr::k_eControllerAxis_Joystick},
{vr::k_EButton_SteamVR_Trigger, vr::k_eControllerAxis_Trigger},
{vr::k_EButton_Axis2, vr::k_eControllerAxis_TrackPad},
};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_webxr_gamepad_support"));
EnterSessionWithUserGestureOrFail();
// Setup some state on the optional buttons (as TestGamepadMinimumData should
// ensure proper state on the required buttons).
// Set a value on the secondary set of axes.
my_mock.SetAxes(controller_index, vr::k_EButton_Axis2, 0.25, -0.25);
// Set the secondary trackpad/joystick to be touched.
my_mock.ToggleButtonTouches(controller_index,
vr::ButtonMaskFromId(vr::k_EButton_Axis2));
// Set the grip button to be pressed.
my_mock.ToggleButtons(controller_index,
vr::ButtonMaskFromId(vr::k_EButton_Grip));
// The secondary set of axes should be set appropriately.
ExecuteStepAndWait("validateAxesValues(1, 0.25, -0.25)");
// Button 2 is reserved for the Grip, and should be pressed.
ExecuteStepAndWait("validateButtonPressed(2)");
// Button 3 is reserved for the secondary trackpad/joystick and should be
// touched but not pressed.
ExecuteStepAndWait("validateButtonNotPressed(3)");
ExecuteStepAndWait("validateButtonTouched(3)");
RunJavaScriptOrFail("done()");
EndTest();
}
// Ensure that if a Gamepad has all required buttons, an extra button not
// mapped in the xr-standard specification, and is missing reserved buttons
// from the XR Standard specification, that the extra button does not appear
// in either of the reserved button slots.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestGamepadReservedData) {
WebXrControllerInputMock my_mock;
// Create a controller that is missing reserved buttons, but supports an
// extra button to guarantee that the reserved button is held.
uint64_t supported_buttons =
vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger) |
vr::ButtonMaskFromId(vr::k_EButton_Axis0) |
vr::ButtonMaskFromId(vr::k_EButton_A);
std::map<vr::EVRButtonId, unsigned int> axis_types = {
{vr::k_EButton_Axis0, vr::k_eControllerAxis_Joystick},
{vr::k_EButton_SteamVR_Trigger, vr::k_eControllerAxis_Trigger},
};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_webxr_gamepad_support"));
EnterSessionWithUserGestureOrFail();
// Claim that all buttons are pressed, note that any non-supported buttons
// should be ignored.
my_mock.ToggleButtons(controller_index, UINT64_MAX);
// Index 2 and 3 are reserved for the grip and secondary joystick.
// As our controller doesn't support them, they should be present but not
// pressed, and our "extra" button should be index 4 and should be pressed.
ExecuteStepAndWait("validateButtonPressed(0)");
ExecuteStepAndWait("validateButtonPressed(1)");
ExecuteStepAndWait("validateButtonNotPressed(2)");
ExecuteStepAndWait("validateButtonNotPressed(3)");
ExecuteStepAndWait("validateButtonPressed(4)");
RunJavaScriptOrFail("done()");
EndTest();
}
// Test that OpenVR controller input is registered via WebXR's input method.
// Equivalent to
// WebXrVrInputTest#testControllerClicksRegisteredOnDaydream_WebXr.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard,
TestControllerInputRegistered) {
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
// Load the test page and enter presentation.
LoadUrlAndAwaitInitialization(GetFileUrlForHtmlTestFile("test_webxr_input"));
EnterSessionWithUserGestureOrFail();
unsigned int num_iterations = 10;
RunJavaScriptOrFail("stepSetupListeners(" +
base::NumberToString(num_iterations) + ")");
// Press and unpress the controller's trigger a bunch of times and make sure
// they're all registered.
for (unsigned int i = 0; i < num_iterations; ++i) {
my_mock.PressReleasePrimaryTrigger(controller_index);
// After each trigger release, wait for the JavaScript to receive the
// "select" event.
WaitOnJavaScriptStep();
}
EndTest();
}
// Test that OpenVR controller input is registered via the Gamepad API.
// Equivalent to
// WebXrVrInputTest#testControllerClicksRegisteredOnDaydream
IN_PROC_BROWSER_TEST_F(WebVrBrowserTestStandard,
TestControllerInputRegistered) {
WebXrControllerInputMock my_mock;
// Connect a controller.
auto controller_data = my_mock.CreateValidController(
device::ControllerRole::kControllerRoleRight);
// openvr_gamepad_helper assumes axis index 1 is the trigger, so we need to
// set that here, otherwise it won't check whether it's pressed or not.
// Note that we aren't able to use the CreateAndConnectMinimalGamepad helper
// here as that adds support for axis_data[0], which causes OpenVR on WebVR
// to treat that button as the primary input (per the defacto webvr standard),
// and we want it to only see the trigger.
controller_data.axis_data[1].axis_type = vr::k_eControllerAxis_Trigger;
unsigned int controller_index = my_mock.ConnectController(controller_data);
// Load the test page and enter presentation.
LoadUrlAndAwaitInitialization(
GetFileUrlForHtmlTestFile("test_gamepad_button"));
EnterSessionWithUserGestureOrFail();
// We need to have this, otherwise the JavaScript side of the Gamepad API
// doesn't seem to pick up the correct button state? I.e. if we don't have
// this, openvr_gamepad_helper properly sets the gamepad's button state,
// but JavaScript still shows no buttons pressed.
// TODO(bsheedy): Figure out why this is the case.
my_mock.PressReleasePrimaryTrigger(controller_index);
// Setting this in the Android version of the test needs to happen after a
// flakiness workaround. Coincidentally, it's also helpful for the different
// issue solved by the above PressReleasePrimaryTrigger, so make sure to set
// it here so that the above press/release isn't caught by the test code.
RunJavaScriptOrFail("canStartTest = true");
// Press and release the trigger, ensuring the Gamepad API detects both.
my_mock.TogglePrimaryTrigger(controller_index);
WaitOnJavaScriptStep();
my_mock.TogglePrimaryTrigger(controller_index);
WaitOnJavaScriptStep();
EndTest();
}
class WebXrHeadPoseMock : public MockXRDeviceHookBase {
public:
void WaitGetPresentingPose(
device_test::mojom::XRTestHook::WaitGetPresentingPoseCallback callback)
final {
auto pose = device_test::mojom::PoseFrameData::New();
pose->device_to_origin = pose_;
std::move(callback).Run(std::move(pose));
}
void SetHeadPose(const gfx::Transform& pose) { pose_ = pose; }
private:
gfx::Transform pose_;
};
std::string TransformToColMajorString(gfx::Transform& t) {
float array[16];
t.matrix().asColMajorf(array);
std::string array_string = "[";
for (int i = 0; i < 16; i++) {
array_string += base::NumberToString(array[i]) + ",";
}
array_string.pop_back();
array_string.push_back(']');
return array_string;
}
void TestHeadPosesUpdateImpl(WebXrVrBrowserTestBase* t) {
WebXrHeadPoseMock my_mock;
t->LoadUrlAndAwaitInitialization(
t->GetFileUrlForHtmlTestFile("webxr_test_head_poses"));
t->EnterSessionWithUserGestureOrFail();
auto pose = gfx::Transform();
my_mock.SetHeadPose(pose);
t->RunJavaScriptOrFail("stepWaitForMatchingPose(" +
TransformToColMajorString(pose) + ")");
t->WaitOnJavaScriptStep();
// No significance to this new transform other than that it's easy to tell
// whether the correct pose got piped through to WebXR or not.
pose.RotateAboutXAxis(90);
pose.Translate3d(2, 3, 4);
my_mock.SetHeadPose(pose);
t->RunJavaScriptOrFail("stepWaitForMatchingPose(" +
TransformToColMajorString(pose) + ")");
t->WaitOnJavaScriptStep();
t->AssertNoJavaScriptErrors();
}
// Test that head pose changes in OpenVR are properly reflected in the viewer
// pose provided by WebXR.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestStandard, TestHeadPosesUpdate) {
TestHeadPosesUpdateImpl(this);
}
// Tests that head pose changes in WMR are properly reflected in the viewer pose
// provided by WebXR.
IN_PROC_BROWSER_TEST_F(WebXrVrBrowserTestWMR, TestHeadPosesUpdate) {
TestHeadPosesUpdateImpl(this);
}
} // namespace vr