blob: 622708589c0bf158d5e2ea87288ee7283df5ac2c [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/multi_class_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.
namespace vr {
// Helper function for verifying the XRInputSource.profiles array contents.
void VerifyInputSourceProfilesArray(
WebXrVrBrowserTestBase* t,
const std::vector<std::string>& expected_values) {
t->PollJavaScriptBooleanOrFail(
"isProfileCountEqualTo(" + base::NumberToString(expected_values.size()) +
")",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// We don't expect the contents of the profiles array to change once we've
// verified its size above, so we can check the expressions a single time
// here instead of polling them.
for (size_t i = 0; i < expected_values.size(); ++i) {
ASSERT_TRUE(t->RunJavaScriptAndExtractBoolOrFail(
"isProfileEqualTo(" + base::NumberToString(i) + ", '" +
expected_values[i] + "')"));
}
}
void VerifyInputCounts(WebXrVrBrowserTestBase* t,
unsigned int expected_input_sources,
unsigned int expected_gamepads) {
t->PollJavaScriptBooleanOrFail("inputSourceCount() === " +
base::NumberToString(expected_input_sources));
t->PollJavaScriptBooleanOrFail("inputSourceWithGamepadCount() === " +
base::NumberToString(expected_gamepads));
}
// Test that focus is locked to the presenting display for the purposes of VR/XR
// input.
void TestPresentationLocksFocusImpl(WebXrVrBrowserTestBase* t,
std::string filename) {
t->LoadFileAndAwaitInitialization(filename);
t->EnterSessionWithUserGestureOrFail();
t->ExecuteStepAndWait("stepSetupFocusLoss()");
t->EndTest();
}
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestPresentationLocksFocus) {
TestPresentationLocksFocusImpl(t, "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, device::XrButtonId button_id) {
auto controller_data = GetCurrentControllerData(index);
uint64_t button_mask = device::XrButtonMaskFromId(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 = device::XrAxisOffsetFromId(button_id);
DCHECK(controller_data.axis_data[axis_offset].axis_type ==
device::XrAxisType::kTrigger);
controller_data.axis_data[axis_offset].x = is_pressed ? 1.0 : 0.0;
UpdateControllerAndWait(index, controller_data);
}
void SetAxes(unsigned int index,
device::XrButtonId button_id,
float x,
float y) {
auto controller_data = GetCurrentControllerData(index);
unsigned int axis_offset = device::XrAxisOffsetFromId(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, device::XrButtonId::kAxisTrigger);
}
void PressReleasePrimaryTrigger(unsigned int index) {
TogglePrimaryTrigger(index);
TogglePrimaryTrigger(index);
}
void SetControllerPose(unsigned int index,
const gfx::Transform& device_to_origin,
bool is_valid) {
auto controller_data = GetCurrentControllerData(index);
controller_data.pose_data.is_valid = is_valid;
device_to_origin.matrix().asColMajorf(
controller_data.pose_data.device_to_origin);
UpdateControllerAndWait(index, controller_data);
}
unsigned int CreateAndConnectMinimalGamepad(
device::ControllerRole role =
device::ControllerRole::kControllerRoleRight) {
// Create a controller that only supports select via a trigger, i.e. it has
// just enough data to be considered a gamepad.
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
};
return CreateAndConnectController(role, axis_types, supported_buttons);
}
unsigned int CreateAndConnectController(
device::ControllerRole role,
std::map<device::XrButtonId, 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 = device::XrAxisOffsetFromId(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<device::XrButtonId, 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 = device::XrAxisType::kNone;
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);
}
void UpdateControllerRole(unsigned int controller_index,
device::ControllerRole role) {
auto controller_data = GetCurrentControllerData(controller_index);
controller_data.role = role;
UpdateControllerAndWait(controller_index, controller_data);
}
// A controller is necessary to simulate voice input because of how the test
// API works.
unsigned int CreateVoiceController() {
return CreateAndConnectMinimalGamepad(
device::ControllerRole::kControllerRoleVoice);
}
private:
// kAxisTrackpad is the first entry in XrButtonId that maps to an axis and the
// subsequent entries are also for input axes.
device::XrButtonId GetAxisId(unsigned int offset) {
return static_cast<device::XrButtonId>(device::XrButtonId::kAxisTrackpad +
offset);
}
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 when an input source's handedness changes, an input source change
// event is fired and a new input source is created.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestInputHandednessChange) {
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
t->LoadFileAndAwaitInitialization("test_webxr_input_same_object");
t->EnterSessionWithUserGestureOrFail();
// We should only have seen the first change indicating we have input sources.
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 1",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// We only expect one input source, cache it.
t->RunJavaScriptOrFail("validateInputSourceLength(1)");
t->RunJavaScriptOrFail("updateCachedInputSource(0)");
// Change the handedness from right to left and verify that we get a change
// event. Then cache the new input source.
my_mock.UpdateControllerRole(controller_index,
device::ControllerRole::kControllerRoleLeft);
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 2",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->RunJavaScriptOrFail("validateCachedSourcePresence(false)");
t->RunJavaScriptOrFail("validateInputSourceLength(1)");
t->RunJavaScriptOrFail("updateCachedInputSource(0)");
// Switch back to the right hand and confirm that we get the change.
my_mock.UpdateControllerRole(controller_index,
device::ControllerRole::kControllerRoleRight);
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 3",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->RunJavaScriptOrFail("validateCachedSourcePresence(false)");
t->RunJavaScriptOrFail("validateInputSourceLength(1)");
t->RunJavaScriptOrFail("done()");
t->EndTest();
}
// Test that inputsourceschange events contain only the expected added/removed
// input sources when a mock controller is connected/disconnected.
// Also validates that if an input source changes substantially we get an event
// containing both the removal of the old one and the additon of the new one,
// rather than two events.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestInputSourcesChange) {
WebXrControllerInputMock my_mock;
// TODO(crbug.com/963676): Figure out if the race is a product or test bug.
// There's a potential for a race causing the input sources change event to
// fire multiple times if we disconnect a controller that has a gamepad.
// Even just a select trigger is sufficient to have an xr-standard mapping, so
// just expose a grip trigger instead so that we don't connect a gamepad.
uint64_t insufficient_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> insufficient_axis_types = {};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, insufficient_axis_types,
insufficient_buttons);
t->LoadFileAndAwaitInitialization("test_webxr_input_sources_change_event");
t->EnterSessionWithUserGestureOrFail();
// Wait for the first changed event
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 1",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Validate that we only have one controller added, and no controller removed.
t->RunJavaScriptOrFail("validateAdded(1)");
t->RunJavaScriptOrFail("validateRemoved(0)");
t->RunJavaScriptOrFail("updateCachedInputSource(0)");
t->RunJavaScriptOrFail("validateCachedAddedPresence(true)");
// Disconnect the controller and validate that we only have one controller
// removed, and that our previously cached controller is in the removed array.
my_mock.DisconnectController(controller_index);
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 2",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->RunJavaScriptOrFail("validateAdded(0)");
t->RunJavaScriptOrFail("validateRemoved(1)");
t->RunJavaScriptOrFail("validateCachedRemovedPresence(true)");
// Connect a controller, and then change enough properties that the system
// recalculates its status as a valid controller, so that we can verify
// it is both added and removed.
// Since we're changing the controller state without disconnecting it, we can
// (and should) use the minimal gamepad here.
controller_index = my_mock.CreateAndConnectMinimalGamepad();
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 3",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->RunJavaScriptOrFail("updateCachedInputSource(0)");
// At least currently, there is no way for WMR/OpenXR to have insufficient
// buttons for a gamepad as long as a controller is connected, so skip this
// part on WMR/OpenXR since it'll always fail
if (t->GetRuntimeType() != XrBrowserTestBase::RuntimeType::RUNTIME_WMR &&
t->GetRuntimeType() != XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR) {
my_mock.UpdateControllerSupport(controller_index, insufficient_axis_types,
insufficient_buttons);
t->PollJavaScriptBooleanOrFail("inputChangeEvents === 4",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->RunJavaScriptOrFail("validateAdded(1)");
t->RunJavaScriptOrFail("validateRemoved(1)");
t->RunJavaScriptOrFail("validateCachedAddedPresence(false)");
t->RunJavaScriptOrFail("validateCachedRemovedPresence(true)");
}
t->RunJavaScriptOrFail("done()");
t->EndTest();
}
// Ensure that when an input source's profiles array changes, an input source
// change event is fired and a new input source is created.
// OpenVR-only since WMR/OpenXR only supports one kind of gamepad, so it's not
// possible to update the connected gamepad functionality to force the profiles
// array to change.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, TestInputProfilesChange) {
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
LoadFileAndAwaitInitialization("test_webxr_input_same_object");
EnterSessionWithUserGestureOrFail();
// Wait for the first changed event
PollJavaScriptBooleanOrFail("inputChangeEvents === 1",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// We only expect one input source, cache it.
RunJavaScriptOrFail("validateInputSourceLength(1)");
RunJavaScriptOrFail("updateCachedInputSource(0)");
// Add a touchpad so that the profiles array changes and verify that we get a
// change event.
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
};
my_mock.UpdateControllerSupport(controller_index, axis_types,
supported_buttons);
PollJavaScriptBooleanOrFail("inputChangeEvents === 2",
WebXrVrBrowserTestBase::kPollTimeoutShort);
RunJavaScriptOrFail("validateCachedSourcePresence(false)");
RunJavaScriptOrFail("validateInputSourceLength(1)");
RunJavaScriptOrFail("done()");
EndTest();
}
// 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.
// OpenVR-only since WMR/OpenXR doesn't support the notion of an incomplete
// gamepad except if using voice input.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, 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).
// Even just setting the select trigger is now enough to create an xr-standard
// gamepad, so we only set the grip trigger in this case.
uint64_t insufficient_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> insufficient_axis_types = {};
// 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 =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad);
std::map<device::XrButtonId, unsigned int> sufficient_axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
};
// Start off without a gamepad.
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, insufficient_axis_types,
insufficient_buttons);
LoadFileAndAwaitInitialization("test_webxr_input_same_object");
EnterSessionWithUserGestureOrFail();
// 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.ToggleButtons(controller_index, insufficient_buttons);
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.
// OpenVR-only because WMR/OpenXR does not currently support the notion of
// incomplete gamepads other than voice input.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, 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 =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger);
my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, {}, supported_buttons);
LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
EnterSessionWithUserGestureOrFail();
PollJavaScriptBooleanOrFail("inputSourceHasNoGamepad()", kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isProfileCountEqualTo(0)", kPollTimeoutShort);
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.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestGamepadMinimumData) {
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
t->LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
t->EnterSessionWithUserGestureOrFail();
VerifyInputCounts(t, 1, 1);
// We only actually connect the data for the one button, but WMR/OpenXR
// expects the WMR/OpenXR controller (which has all of the required and
// optional buttons) and so adds dummy/placeholder buttons regardless of what
// data we send up.
std::string button_count = "1";
if (t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_WMR ||
t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR)
button_count = "4";
t->PollJavaScriptBooleanOrFail("isButtonCountEqualTo(" + button_count + ")",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// 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);
// The trigger should be button 0.
t->PollJavaScriptBooleanOrFail("isMappingEqualTo('xr-standard')",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
if (t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_WMR) {
// WMR will still report having grip, touchpad, and thumbstick because it
// only supports that type of controller and fills in default values if
// those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENVR) {
VerifyInputSourceProfilesArray(
t, {"test-value-test-value", "generic-trigger"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR) {
// OpenXR will still report having squeeze, menu, touchpad, and thumbstick
// because it only supports that type of controller and fills in default
// values if those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
}
t->RunJavaScriptOrFail("done()");
t->EndTest();
}
// Make sure the input gets plumbed to the correct gamepad, including when
// button presses are interleaved.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestMultipleGamepads) {
WebXrControllerInputMock my_mock;
unsigned int controller_index1 = my_mock.CreateAndConnectMinimalGamepad(
device::ControllerRole::kControllerRoleLeft);
unsigned int controller_index2 = my_mock.CreateAndConnectMinimalGamepad();
t->LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
t->EnterSessionWithUserGestureOrFail();
VerifyInputCounts(t, 2, 2);
// We only actually connect the data for the one button, but WMR/OpenXR
// expects the WMR/OpenXR controller (which has all of the required and
// optional buttons) and so adds dummy/placeholder buttons regardless of what
// data we send up.
std::string button_count = "1";
if (t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_WMR ||
t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR)
button_count = "4";
// Make sure both gamepads have the expected button count and mapping.
ASSERT_TRUE(t->RunJavaScriptAndExtractBoolOrFail("isButtonCountEqualTo(" +
button_count + ", 0)"));
ASSERT_TRUE(t->RunJavaScriptAndExtractBoolOrFail("isButtonCountEqualTo(" +
button_count + ", 1)"));
ASSERT_TRUE(t->RunJavaScriptAndExtractBoolOrFail(
"isMappingEqualTo('xr-standard', 0)"));
ASSERT_TRUE(t->RunJavaScriptAndExtractBoolOrFail(
"isMappingEqualTo('xr-standard', 1)"));
// 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_index1);
// The trigger should be button 0. Make sure it is only pressed on the first
// gamepad.
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, true, 0)");
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, false, 1)");
// Now press the other gamepad's button and make sure it's registered.
my_mock.TogglePrimaryTrigger(controller_index2);
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, true, 1)");
// Then release the trigger. The second gamepad's button should no longer be
// pressed, but the first gamepad's button should still be pressed because we
// haven't released that trigger yet.
my_mock.TogglePrimaryTrigger(controller_index2);
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, false, 1)");
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, true, 0)");
// Finally, release the trigger on the first gamepad.
my_mock.TogglePrimaryTrigger(controller_index1);
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, false, 0)");
if (t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_WMR) {
// WMR will still report having grip, touchpad, and thumbstick because it
// only supports that type of controller and fills in default values if
// those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENVR) {
VerifyInputSourceProfilesArray(
t, {"test-value-test-value", "generic-trigger"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR) {
// OpenXR will still report having squeeze, menu, touchpad, and thumbstick
// because it only supports that type of controller and fills in default
// values if those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
}
t->RunJavaScriptOrFail("done()");
t->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.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestGamepadCompleteData) {
WebXrControllerInputMock my_mock;
// Create a controller that supports all reserved buttons.
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisThumbstick) |
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
{device::XrButtonId::kAxisThumbstick, device::XrAxisType::kJoystick},
};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
t->LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
t->EnterSessionWithUserGestureOrFail();
VerifyInputCounts(t, 1, 1);
// Setup some state on the optional buttons (as TestGamepadMinimumData should
// ensure proper state on the required buttons).
// Set a value on the touchpad.
my_mock.SetAxes(controller_index, device::XrButtonId::kAxisTrackpad, 0.25,
-0.25);
// Set the touchpad to be touched.
my_mock.ToggleButtonTouches(
controller_index,
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad));
// Also test the thumbstick.
my_mock.SetAxes(controller_index, device::XrButtonId::kAxisThumbstick, 0.67,
-0.67);
my_mock.ToggleButtons(
controller_index,
device::XrButtonMaskFromId(device::XrButtonId::kAxisThumbstick));
// Set the grip button to be pressed.
my_mock.ToggleButtons(controller_index,
device::XrButtonMaskFromId(device::XrButtonId::kGrip));
// Controller should meet the requirements for the 'xr-standard' mapping.
t->PollJavaScriptBooleanOrFail("isMappingEqualTo('xr-standard')",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Controller should have all required and optional xr-standard buttons
t->PollJavaScriptBooleanOrFail("isButtonCountEqualTo(4)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// The touchpad axes should be set appropriately.
t->PollJavaScriptBooleanOrFail("areAxesValuesEqualTo(0, 0.25, -0.25)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// The thumbstick axes should be set appropriately.
t->PollJavaScriptBooleanOrFail("areAxesValuesEqualTo(1, 0.67, -0.67)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Button 1 is reserved for the Grip, and should be pressed.
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(1, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Button 2 is reserved for the trackpad and should be touched but not
// pressed.
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(2, false)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->PollJavaScriptBooleanOrFail("isButtonTouchedEqualTo(2, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Button 3 is reserved for the thumbstick and should be touched and pressed.
t->PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(3, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
t->PollJavaScriptBooleanOrFail("isButtonTouchedEqualTo(3, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
if (t->GetRuntimeType() == XrBrowserTestBase::RuntimeType::RUNTIME_WMR) {
// WMR will still report having grip, touchpad, and thumbstick because it
// only supports that type of controller and fills in default values if
// those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENVR) {
VerifyInputSourceProfilesArray(
t, {"test-value-test-value",
"generic-trigger-squeeze-touchpad-thumbstick"});
} else if (t->GetRuntimeType() ==
XrBrowserTestBase::RuntimeType::RUNTIME_OPENXR) {
// OpenXR will still report having squeeze, menu, touchpad, and thumbstick
// because it only supports that type of controller and fills in default
// values if those inputs don't exist.
VerifyInputSourceProfilesArray(
t, {"windows-mixed-reality",
"generic-trigger-squeeze-touchpad-thumbstick"});
}
t->RunJavaScriptOrFail("done()");
t->EndTest();
}
// Tests that axes data is still reported on the secondary axes even if
// the button is not supported (we see this case with WMR through OpenVR where
// the secondary axes button is reserved by the system, but we still get valid
// data for the axes, there may be other controllers where this is the case).
// Because this is specifically a bug in the OpenVR runtime/with configurable
// controllers, not testing WMR/OpenXR.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, TestInputAxesWithNoButton) {
WebXrControllerInputMock my_mock;
// Create a controller that supports all reserved buttons, except the
// secondary axis. (Though it is a valid axis)
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad) |
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
{device::XrButtonId::kAxisThumbstick, device::XrAxisType::kJoystick},
};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
EnterSessionWithUserGestureOrFail();
VerifyInputCounts(this, 1, 1);
// 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, device::XrButtonId::kAxisThumbstick, 0.25,
-0.25);
// Controller should meet the requirements for the 'xr-standard' mapping.
PollJavaScriptBooleanOrFail("isMappingEqualTo('xr-standard')",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// Controller should have all required and optional xr-standard buttons
PollJavaScriptBooleanOrFail("isButtonCountEqualTo(4)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// The secondary set of axes should be set appropriately.
PollJavaScriptBooleanOrFail("areAxesValuesEqualTo(1, 0.25, -0.25)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
// If we have a non-zero axis value, the button should be touched.
PollJavaScriptBooleanOrFail("isButtonTouchedEqualTo(3, true)",
WebXrVrBrowserTestBase::kPollTimeoutShort);
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. OpenVR-only since WMR/OpenXR only
// supports one controller type.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, 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 =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad) |
device::XrButtonMaskFromId(device::XrButtonId::kA);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
};
unsigned int controller_index = my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
EnterSessionWithUserGestureOrFail();
VerifyInputCounts(this, 1, 1);
// Claim that all buttons are pressed, note that any non-supported buttons
// should be ignored.
my_mock.ToggleButtons(controller_index, UINT64_MAX);
// Index 1 and 3 are reserved for the grip and 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.
PollJavaScriptBooleanOrFail("isMappingEqualTo('xr-standard')",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonCountEqualTo(5)", kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(0, true)",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(1, false)",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(2, true)",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(3, false)",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonPressedEqualTo(4, true)",
kPollTimeoutShort);
VerifyInputSourceProfilesArray(
this, {"test-value-test-value", "generic-trigger-touchpad"});
RunJavaScriptOrFail("done()");
EndTest();
}
// Ensure that if a gamepad has a grip, but not any extra buttons or a secondary
// axis, that no trailing placeholder button is added. This is a slight
// variation on TestGamepadMinimalData, but won't re-test whether or not buttons
// get sent up. Note that since WMR/OpenXR always builds the WMR/OpenXR
// controller which supports all required and optional buttons specified by the
// xr-standard mapping, this test is OpenVR-only.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenVrBrowserTest, TestGamepadOptionalData) {
WebXrControllerInputMock my_mock;
// Create a controller that supports the trigger, primary axis, and grip
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad) |
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
};
my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
LoadFileAndAwaitInitialization("test_webxr_gamepad_support");
EnterSessionWithUserGestureOrFail();
VerifyInputCounts(this, 1, 1);
// There should be enough buttons for an xr-standard mapping, and it should
// have one optional button, but not the other.
PollJavaScriptBooleanOrFail("isMappingEqualTo('xr-standard')",
kPollTimeoutShort);
PollJavaScriptBooleanOrFail("isButtonCountEqualTo(3)", kPollTimeoutShort);
VerifyInputSourceProfilesArray(
this, {"test-value-test-value", "generic-trigger-squeeze-touchpad"});
RunJavaScriptOrFail("done()");
EndTest();
}
#if BUILDFLAG(ENABLE_OPENXR)
// Ensure that if OpenXR Runtime receive interaction profile chagnes event,
// input profile name will be changed accordingly.
IN_PROC_BROWSER_TEST_F(WebXrVrOpenXrBrowserTest,
TestInteractionProfileChanged) {
WebXrControllerInputMock my_mock;
// Create a controller that supports all reserved buttons.
uint64_t supported_buttons =
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrigger) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisTrackpad) |
device::XrButtonMaskFromId(device::XrButtonId::kAxisThumbstick) |
device::XrButtonMaskFromId(device::XrButtonId::kGrip);
std::map<device::XrButtonId, unsigned int> axis_types = {
{device::XrButtonId::kAxisTrackpad, device::XrAxisType::kTrackpad},
{device::XrButtonId::kAxisTrigger, device::XrAxisType::kTrigger},
{device::XrButtonId::kAxisThumbstick, device::XrAxisType::kJoystick},
};
my_mock.CreateAndConnectController(
device::ControllerRole::kControllerRoleRight, axis_types,
supported_buttons);
this->LoadFileAndAwaitInitialization("test_webxr_input_same_object");
this->EnterSessionWithUserGestureOrFail();
// 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.
this->RunJavaScriptOrFail("validateInputSourceLength(1)");
this->RunJavaScriptOrFail("updateCachedInputSource(0)");
// Simulate Runtimes Sends change interaction profile event to change from
// Windows motion controller to Khronos simple Controller.
device_test::mojom::EventData data = {};
data.type = device_test::mojom::EventType::kInteractionProfileChanged;
data.interaction_profile =
device_test::mojom::InteractionProfileType::kKHRSimple;
my_mock.PopulateEvent(data);
// Make sure change events happens again since interaction profile changedd
PollJavaScriptBooleanOrFail("inputChangeEvents === 2", kPollTimeoutShort);
this->RunJavaScriptOrFail("validateInputSourceLength(1)");
this->RunJavaScriptOrFail("validateCachedSourcePresence(false)");
this->RunJavaScriptOrFail("done()");
this->EndTest();
}
#endif // BUILDFLAG(ENABLE_OPENXR)
// Test that controller input is registered via WebXR's input method. This uses
// multiple controllers to make sure the input is going to the correct one.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestMultipleControllerInputRegistered) {
WebXrControllerInputMock my_mock;
unsigned int controller_index1 = my_mock.CreateAndConnectMinimalGamepad(
device::ControllerRole::kControllerRoleLeft);
unsigned int controller_index2 = my_mock.CreateAndConnectMinimalGamepad();
// Load the test page and enter presentation.
t->LoadFileAndAwaitInitialization("test_webxr_input");
t->EnterSessionWithUserGestureOrFail();
t->RunJavaScriptOrFail("stepSetupListeners(2)");
// Press and release the first controller's trigger and make sure the select
// events are registered for it. After trigger release, must wait for JS to
// receive the "select" event.
t->RunJavaScriptOrFail("expectedInputSourceIndex = 0");
my_mock.PressReleasePrimaryTrigger(controller_index1);
t->WaitOnJavaScriptStep();
// Do the same thing for the other controller.
t->RunJavaScriptOrFail("expectedInputSourceIndex = 1");
my_mock.PressReleasePrimaryTrigger(controller_index2);
t->WaitOnJavaScriptStep();
t->EndTest();
}
// Test that voice input is registered via WebXR's input method. WMR only since
// it's the only platform we are testing that supports select via voice.
IN_PROC_BROWSER_TEST_F(WebXrVrWmrBrowserTest, TestVoiceSelectRegistered) {
WebXrControllerInputMock my_mock;
unsigned int index = my_mock.CreateVoiceController();
// Load the test page and enter presentation.
LoadFileAndAwaitInitialization("test_webxr_input");
EnterSessionWithUserGestureOrFail();
RunJavaScriptOrFail("stepSetupListeners(1)");
// Simulate the user saying "select" and make sure the select events are
// registered for it. Must wait for JS to receive the "select" event.
my_mock.PressReleasePrimaryTrigger(index);
WaitOnJavaScriptStep();
EndTest();
}
// Test that controller input is registered via WebXR's input method.
// Equivalent to
// WebXrVrInputTest#testControllerClicksRegisteredOnDaydream_WebXr.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestControllerInputRegistered) {
// TODO(crbug.com/1033087): Test is flaky on OpenVR
WEBXR_VR_DISABLE_TEST_ON(XrBrowserTestBase::RuntimeType::RUNTIME_OPENVR);
WebXrControllerInputMock my_mock;
unsigned int controller_index = my_mock.CreateAndConnectMinimalGamepad();
// Load the test page and enter presentation.
t->LoadFileAndAwaitInitialization("test_webxr_input");
t->EnterSessionWithUserGestureOrFail();
unsigned int num_iterations = 5;
t->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.
t->WaitOnJavaScriptStep();
}
t->EndTest();
}
std::string TransformToColMajorString(const 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;
}
// Test that changes in controller position are properly plumbed through to
// WebXR.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestControllerPositionTracking) {
WebXrControllerInputMock my_mock;
auto controller_data = my_mock.CreateValidController(
device::ControllerRole::kControllerRoleRight);
unsigned int controller_index = my_mock.ConnectController(controller_data);
t->LoadFileAndAwaitInitialization("webxr_test_controller_poses");
t->EnterSessionWithUserGestureOrFail();
auto pose = gfx::Transform();
pose.RotateAboutXAxis(90);
pose.RotateAboutYAxis(45);
pose.RotateAboutZAxis(180);
pose.Translate3d(0.5f, 2, -3);
my_mock.SetControllerPose(controller_index, pose, true);
// Apply any offset we expect the runtime to add.
pose.Translate3d(t->GetControllerOffset());
t->ExecuteStepAndWait("stepWaitForMatchingPose(" +
base::NumberToString(controller_index - 1) + ", " +
TransformToColMajorString(pose) + ")");
t->AssertNoJavaScriptErrors();
}
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_;
};
// Test that head pose changes are properly reflected in the viewer pose
// provided by WebXR.
WEBXR_VR_ALL_RUNTIMES_BROWSER_TEST_F(TestHeadPosesUpdate) {
WebXrHeadPoseMock my_mock;
t->LoadFileAndAwaitInitialization("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();
}
} // namespace vr