blob: 2cd4db9a1dbe5ca1e1ac9a28afed646f2fae56e8 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <fuchsia/input/virtualkeyboard/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl_test_base.h>
#include <memory>
#include "base/fuchsia/scoped_service_binding.h"
#include "base/fuchsia/test_component_context_for_process.h"
#include "base/test/scoped_feature_list.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "fuchsia_web/common/test/frame_test_util.h"
#include "fuchsia_web/common/test/test_navigation_listener.h"
#include "fuchsia_web/webengine/browser/context_impl.h"
#include "fuchsia_web/webengine/features.h"
#include "fuchsia_web/webengine/test/frame_for_test.h"
#include "fuchsia_web/webengine/test/scenic_test_helper.h"
#include "fuchsia_web/webengine/test/scoped_connection_checker.h"
#include "fuchsia_web/webengine/test/test_data.h"
#include "fuchsia_web/webengine/test/web_engine_browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
using fuchsia::input::Key;
using fuchsia::ui::input3::KeyEvent;
using fuchsia::ui::input3::KeyEventType;
using fuchsia::ui::input3::KeyMeaning;
using fuchsia::ui::input3::NonPrintableKey;
namespace {
const char kKeyDown[] = "keydown";
const char kKeyPress[] = "keypress";
const char kKeyUp[] = "keyup";
const char kKeyDicts[] = "keyDicts";
// Returns a KeyEvent with |key_meaning| set based on the supplied codepoint,
// the |key| field left not set.
KeyEvent CreateCharacterKeyEvent(uint32_t codepoint, KeyEventType event_type) {
KeyEvent key_event;
fuchsia::ui::input3::KeyMeaning meaning;
meaning.set_codepoint(codepoint);
key_event.set_key_meaning(std::move(meaning));
key_event.set_type(event_type);
key_event.set_timestamp(base::TimeTicks::Now().ToZxTime());
return key_event;
}
struct KeyEventOptions {
bool repeat;
};
// Returns a KeyEvent with both |key| and |key_meaning| set.
KeyEvent CreateKeyEvent(Key key,
KeyMeaning key_meaning,
KeyEventType event_type,
KeyEventOptions options = {}) {
KeyEvent key_event;
key_event.set_timestamp(base::TimeTicks::Now().ToZxTime());
key_event.set_type(event_type);
key_event.set_key(key);
key_event.set_key_meaning(std::move(key_meaning));
if (options.repeat) {
// Chromium doesn't look at the value of this, it just check if the field is
// present.
key_event.set_repeat_sequence(1);
}
return key_event;
}
KeyEvent CreateKeyEvent(Key key,
uint32_t codepoint,
KeyEventType event_type,
KeyEventOptions options = {}) {
return CreateKeyEvent(key, KeyMeaning::WithCodepoint(std::move(codepoint)),
event_type, options);
}
KeyEvent CreateKeyEvent(Key key,
NonPrintableKey non_printable_key,
KeyEventType event_type,
KeyEventOptions options = {}) {
return CreateKeyEvent(
key, KeyMeaning::WithNonPrintableKey(std::move(non_printable_key)),
event_type, options);
}
base::Value ExpectedKeyValue(base::StringPiece code,
base::StringPiece key,
base::StringPiece type,
KeyEventOptions options = {}) {
base::Value::Dict expected;
expected.Set("code", code);
expected.Set("key", key);
expected.Set("type", type);
expected.Set("repeat", options.repeat);
return base::Value(std::move(expected));
}
class FakeKeyboard : public fuchsia::ui::input3::testing::Keyboard_TestBase {
public:
explicit FakeKeyboard(sys::OutgoingDirectory* additional_services)
: binding_(additional_services, this) {}
~FakeKeyboard() override = default;
FakeKeyboard(const FakeKeyboard&) = delete;
FakeKeyboard& operator=(const FakeKeyboard&) = delete;
base::ScopedServiceBinding<fuchsia::ui::input3::Keyboard>* binding() {
return &binding_;
}
// Sends |key_event| to |listener_|;
void SendKeyEvent(KeyEvent key_event) {
listener_->OnKeyEvent(std::move(key_event),
[num_sent_events = num_sent_events_,
this](fuchsia::ui::input3::KeyEventStatus status) {
ASSERT_EQ(num_acked_events_, num_sent_events)
<< "Key events are acked out of order";
num_acked_events_++;
});
num_sent_events_++;
}
// fuchsia::ui::input3::Keyboard implementation.
void AddListener(
fuchsia::ui::views::ViewRef view_ref,
fidl::InterfaceHandle<::fuchsia::ui::input3::KeyboardListener> listener,
AddListenerCallback callback) final {
// This implementation is only set up to have up to one listener.
EXPECT_FALSE(listener_);
listener_ = listener.Bind();
callback();
}
void NotImplemented_(const std::string& name) final {
NOTIMPLEMENTED() << name;
}
private:
fuchsia::ui::input3::KeyboardListenerPtr listener_;
base::ScopedServiceBinding<fuchsia::ui::input3::Keyboard> binding_;
// Counters to make sure key events are acked in order.
int num_sent_events_ = 0;
int num_acked_events_ = 0;
};
class KeyboardInputTest : public WebEngineBrowserTest {
public:
KeyboardInputTest() { set_test_server_root(base::FilePath(kTestServerRoot)); }
~KeyboardInputTest() override = default;
KeyboardInputTest(const KeyboardInputTest&) = delete;
KeyboardInputTest& operator=(const KeyboardInputTest&) = delete;
protected:
virtual void SetUpService() {
keyboard_service_.emplace(component_context_->additional_services());
}
void SetUp() override {
scoped_feature_list_.InitWithFeatures({features::kKeyboardInput}, {});
WebEngineBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
WebEngineBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
fuchsia::web::CreateFrameParams params;
frame_for_test_ = FrameForTest::Create(context(), std::move(params));
// Set up services needed for the test. The keyboard service is included in
// the allowed services by default. The real service needs to be removed so
// it can be replaced by this fake implementation.
component_context_.emplace(
base::TestComponentContextForProcess::InitialState::kCloneAll);
component_context_->additional_services()
->RemovePublicService<fuchsia::ui::input3::Keyboard>();
SetUpService();
virtual_keyboard_checker_.emplace(
component_context_->additional_services());
fuchsia::web::NavigationControllerPtr controller;
frame_for_test_.ptr()->GetNavigationController(controller.NewRequest());
const GURL test_url(embedded_test_server()->GetURL("/keyevents.html"));
EXPECT_TRUE(LoadUrlAndExpectResponse(
controller.get(), fuchsia::web::LoadUrlParams(), test_url.spec()));
frame_for_test_.navigation_listener().RunUntilUrlEquals(test_url);
fuchsia::web::FramePtr* frame_ptr = &(frame_for_test_.ptr());
scenic_test_helper_.CreateScenicView(
context_impl()->GetFrameImplForTest(frame_ptr), frame_for_test_.ptr());
scenic_test_helper_.SetUpViewForInteraction(
context_impl()->GetFrameImplForTest(frame_ptr)->web_contents());
}
void TearDownOnMainThread() override {
frame_for_test_ = {};
WebEngineBrowserTest::TearDownOnMainThread();
}
// The tests expect to have input processed immediately, even if the
// content has not been displayed yet. That's fine for the test, but
// we need to explicitly allow it.
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch("allow-pre-commit-input");
}
template <typename... Args>
void ExpectKeyEventsEqual(Args... events) {
base::Value::List expected =
content::ListValueOf(std::forward<Args>(events)...).TakeList();
frame_for_test_.navigation_listener().RunUntilTitleEquals(
base::NumberToString(expected.size()));
absl::optional<base::Value> actual =
ExecuteJavaScript(frame_for_test_.ptr().get(), kKeyDicts);
EXPECT_EQ(*actual, base::Value(std::move(expected)));
}
// Used to publish fake services.
absl::optional<base::TestComponentContextForProcess> component_context_;
FrameForTest frame_for_test_;
ScenicTestHelper scenic_test_helper_;
absl::optional<FakeKeyboard> keyboard_service_;
base::test::ScopedFeatureList scoped_feature_list_;
absl::optional<
NeverConnectedChecker<fuchsia::input::virtualkeyboard::ControllerCreator>>
virtual_keyboard_checker_;
};
// Check that printable keys are sent and received correctly.
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, PrintableKeys) {
// Send key press events from the Fuchsia keyboard service.
// Pressing character keys will generate a JavaScript keydown event followed
// by a keypress event. Releasing any key generates a keyup event.
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::A, 'a', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_8, '8', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_8, '8', KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::A, 'a', KeyEventType::RELEASED));
ExpectKeyEventsEqual(ExpectedKeyValue("KeyA", "a", kKeyDown),
ExpectedKeyValue("KeyA", "a", kKeyPress),
ExpectedKeyValue("Digit8", "8", kKeyDown),
ExpectedKeyValue("Digit8", "8", kKeyPress),
ExpectedKeyValue("Digit8", "8", kKeyUp),
ExpectedKeyValue("KeyA", "a", kKeyUp));
}
// Check that character virtual keys are sent and received correctly.
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, Characters) {
// Send key press events from the Fuchsia keyboard service.
// Pressing character keys will generate a JavaScript keydown event followed
// by a keypress event. Releasing any key generates a keyup event.
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('A', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('A', KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('b', KeyEventType::PRESSED));
ExpectKeyEventsEqual(
ExpectedKeyValue("", "A", kKeyDown), ExpectedKeyValue("", "A", kKeyPress),
ExpectedKeyValue("", "A", kKeyUp), ExpectedKeyValue("", "b", kKeyDown),
ExpectedKeyValue("", "b", kKeyPress));
}
// Verify that character events are not affected by active modifiers.
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, ShiftCharacter) {
// TODO(fxbug.dev/106600): Update the WithCodepoint(0)s when the platform is
// fixed to provide valid KeyMeanings for these keys.
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, 0, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('a', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('a', KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, 0, KeyEventType::RELEASED));
ExpectKeyEventsEqual(
ExpectedKeyValue("ShiftLeft", "Shift", kKeyDown),
ExpectedKeyValue("", "a", kKeyDown), // Remains lowercase.
ExpectedKeyValue("", "a", kKeyPress), // You guessed it! Still lowercase.
ExpectedKeyValue("", "a", kKeyUp), // Wow, lowercase just won't quit.
ExpectedKeyValue("ShiftLeft", "Shift", kKeyUp));
}
// Verifies that codepoints outside the 16-bit Unicode BMP are rejected.
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, CharacterInBmp) {
const wchar_t kSigma = 0x03C3;
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent(kSigma, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent(kSigma, KeyEventType::RELEASED));
std::string expected_utf8;
ASSERT_TRUE(base::WideToUTF8(&kSigma, 1, &expected_utf8));
ExpectKeyEventsEqual(ExpectedKeyValue("", expected_utf8, kKeyDown),
ExpectedKeyValue("", expected_utf8, kKeyPress),
ExpectedKeyValue("", expected_utf8, kKeyUp));
}
// Verifies that codepoints beyond the range of allowable UCS-2 values
// are rejected.
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, CharacterBeyondBmp) {
const uint32_t kRamenEmoji = 0x1F35C;
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent(kRamenEmoji, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent(kRamenEmoji, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('a', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateCharacterKeyEvent('a', KeyEventType::RELEASED));
ExpectKeyEventsEqual(ExpectedKeyValue("", "a", kKeyDown),
ExpectedKeyValue("", "a", kKeyPress),
ExpectedKeyValue("", "a", kKeyUp));
}
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, ShiftPrintableKeys) {
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, 0, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::B, 'B', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_1, '!', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::SPACE, ' ', KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_SHIFT, 0, KeyEventType::RELEASED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::DOT, '.', KeyEventType::PRESSED));
// Note that non-character keys (e.g. shift, control) only generate key down
// and key up web events. They do not generate key pressed events.
ExpectKeyEventsEqual(ExpectedKeyValue("ShiftLeft", "Shift", kKeyDown),
ExpectedKeyValue("KeyB", "B", kKeyDown),
ExpectedKeyValue("KeyB", "B", kKeyPress),
ExpectedKeyValue("Digit1", "!", kKeyDown),
ExpectedKeyValue("Digit1", "!", kKeyPress),
ExpectedKeyValue("Space", " ", kKeyDown),
ExpectedKeyValue("Space", " ", kKeyPress),
ExpectedKeyValue("ShiftLeft", "Shift", kKeyUp),
ExpectedKeyValue("Period", ".", kKeyDown),
ExpectedKeyValue("Period", ".", kKeyPress));
}
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, ShiftNonPrintableKeys) {
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::RIGHT_SHIFT, 0, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(CreateKeyEvent(
Key::ENTER, NonPrintableKey::ENTER, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::LEFT_CTRL, 0, KeyEventType::PRESSED));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::RIGHT_SHIFT, 0, KeyEventType::RELEASED));
// Note that non-character keys (e.g. shift, control) only generate key down
// and key up web events. They do not generate key pressed events.
ExpectKeyEventsEqual(ExpectedKeyValue("ShiftRight", "Shift", kKeyDown),
ExpectedKeyValue("Enter", "Enter", kKeyDown),
ExpectedKeyValue("Enter", "Enter", kKeyPress),
ExpectedKeyValue("ControlLeft", "Control", kKeyDown),
ExpectedKeyValue("ShiftRight", "Shift", kKeyUp));
}
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, RepeatedKeys) {
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::A, 'a', KeyEventType::PRESSED, {.repeat = true}));
keyboard_service_->SendKeyEvent(
CreateKeyEvent(Key::KEY_8, '8', KeyEventType::PRESSED, {.repeat = true}));
// Note that non-character keys (e.g. shift, control) only generate key down
// and key up web events. They do not generate key pressed events.
ExpectKeyEventsEqual(
ExpectedKeyValue("KeyA", "a", kKeyDown, {.repeat = true}),
ExpectedKeyValue("KeyA", "a", kKeyPress, {.repeat = true}),
ExpectedKeyValue("Digit8", "8", kKeyDown, {.repeat = true}),
ExpectedKeyValue("Digit8", "8", kKeyPress, {.repeat = true}));
}
IN_PROC_BROWSER_TEST_F(KeyboardInputTest, Disconnect) {
// Disconnect the keyboard service.
keyboard_service_.reset();
frame_for_test_.navigation_listener().RunUntilTitleEquals("loaded");
// Make sure the page is still available and there are no crashes.
EXPECT_TRUE(
ExecuteJavaScript(frame_for_test_.ptr().get(), "true")->GetBool());
}
class KeyboardInputTestWithoutKeyboardFeature : public KeyboardInputTest {
public:
KeyboardInputTestWithoutKeyboardFeature() = default;
~KeyboardInputTestWithoutKeyboardFeature() override = default;
protected:
void SetUp() override {
scoped_feature_list_.InitWithFeatures({}, {});
WebEngineBrowserTest::SetUp();
}
void SetUpService() override {
keyboard_input_checker_.emplace(component_context_->additional_services());
}
absl::optional<NeverConnectedChecker<fuchsia::ui::input3::Keyboard>>
keyboard_input_checker_;
};
IN_PROC_BROWSER_TEST_F(KeyboardInputTestWithoutKeyboardFeature, NoFeature) {
// Test will verify that |keyboard_input_checker_| never received a connection
// request at teardown time.
}
} // namespace