// Copyright 2014 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 <vector> #include "base/basictypes.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop/message_loop.h" #include "base/time/time.h" #include "content/browser/renderer_host/input/touch_emulator.h" #include "content/browser/renderer_host/input/touch_emulator_client.h" #include "content/common/input/web_input_event_traits.h" #include "testing/gtest/include/gtest/gtest.h" #if defined(USE_AURA) #include "ui/aura/env.h" #include "ui/aura/test/test_screen.h" #endif using blink::WebGestureEvent; using blink::WebInputEvent; using blink::WebKeyboardEvent; using blink::WebMouseEvent; using blink::WebMouseWheelEvent; using blink::WebTouchEvent; using blink::WebTouchPoint; namespace content { class TouchEmulatorTest : public testing::Test, public TouchEmulatorClient { public: TouchEmulatorTest() : shift_pressed_(false), mouse_pressed_(false), ack_touches_synchronously_(true), last_mouse_x_(-1), last_mouse_y_(-1) { last_event_time_seconds_ = (base::TimeTicks::Now() - base::TimeTicks()).InSecondsF(); event_time_delta_seconds_ = 0.1; } ~TouchEmulatorTest() override {} // testing::Test void SetUp() override { #if defined(USE_AURA) aura::Env::CreateInstance(true); screen_.reset(aura::TestScreen::Create(gfx::Size())); gfx::Screen::SetScreenInstance(gfx::SCREEN_TYPE_NATIVE, screen_.get()); #endif emulator_.reset(new TouchEmulator(this)); emulator_->SetDoubleTapSupportForPageEnabled(false); emulator_->Enable(ui::GestureProviderConfigType::GENERIC_MOBILE); } void TearDown() override { emulator_->Disable(); EXPECT_EQ("", ExpectedEvents()); #if defined(USE_AURA) aura::Env::DeleteInstance(); gfx::Screen::SetScreenInstance(gfx::SCREEN_TYPE_NATIVE, nullptr); screen_.reset(); #endif } void ForwardGestureEvent(const blink::WebGestureEvent& event) override { forwarded_events_.push_back(event.type); } void ForwardEmulatedTouchEvent(const blink::WebTouchEvent& event) override { forwarded_events_.push_back(event.type); EXPECT_EQ(1U, event.touchesLength); EXPECT_EQ(last_mouse_x_, event.touches[0].position.x); EXPECT_EQ(last_mouse_y_, event.touches[0].position.y); bool expected_cancelable = event.type != WebInputEvent::TouchCancel; EXPECT_EQ(expected_cancelable, !!event.cancelable); if (ack_touches_synchronously_) { emulator()->HandleTouchEventAck( event, INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS); } } void SetCursor(const WebCursor& cursor) override {} void ShowContextMenuAtPoint(const gfx::Point& point) override {} protected: TouchEmulator* emulator() const { return emulator_.get(); } int modifiers() const { return shift_pressed_ ? WebInputEvent::ShiftKey : 0; } std::string ExpectedEvents() { std::string result; for (size_t i = 0; i < forwarded_events_.size(); ++i) { if (i != 0) result += " "; result += WebInputEventTraits::GetName(forwarded_events_[i]); } forwarded_events_.clear(); return result; } double GetNextEventTimeSeconds() { last_event_time_seconds_ += event_time_delta_seconds_; return last_event_time_seconds_; } void set_event_time_delta_seconds_(double delta) { event_time_delta_seconds_ = delta; } void SendKeyboardEvent(WebInputEvent::Type type) { WebKeyboardEvent event; event.timeStampSeconds = GetNextEventTimeSeconds(); event.type = type; event.modifiers = modifiers(); emulator()->HandleKeyboardEvent(event); } void PressShift() { DCHECK(!shift_pressed_); shift_pressed_ = true; SendKeyboardEvent(WebInputEvent::KeyDown); } void ReleaseShift() { DCHECK(shift_pressed_); shift_pressed_ = false; SendKeyboardEvent(WebInputEvent::KeyUp); } void SendMouseEvent(WebInputEvent::Type type, int x, int y) { WebMouseEvent event; event.timeStampSeconds = GetNextEventTimeSeconds(); event.type = type; event.button = mouse_pressed_ ? WebMouseEvent::ButtonLeft : WebMouseEvent::ButtonNone; event.modifiers = modifiers(); last_mouse_x_ = x; last_mouse_y_ = y; event.x = event.windowX = event.globalX = x; event.y = event.windowY = event.globalY = y; emulator()->HandleMouseEvent(event); } bool SendMouseWheelEvent() { WebMouseWheelEvent event; event.type = WebInputEvent::MouseWheel; event.timeStampSeconds = GetNextEventTimeSeconds(); // Return whether mouse wheel is forwarded. return !emulator()->HandleMouseWheelEvent(event); } void MouseDown(int x, int y) { DCHECK(!mouse_pressed_); if (x != last_mouse_x_ || y != last_mouse_y_) SendMouseEvent(WebInputEvent::MouseMove, x, y); mouse_pressed_ = true; SendMouseEvent(WebInputEvent::MouseDown, x, y); } void MouseDrag(int x, int y) { DCHECK(mouse_pressed_); SendMouseEvent(WebInputEvent::MouseMove, x, y); } void MouseMove(int x, int y) { DCHECK(!mouse_pressed_); SendMouseEvent(WebInputEvent::MouseMove, x, y); } void MouseUp(int x, int y) { DCHECK(mouse_pressed_); if (x != last_mouse_x_ || y != last_mouse_y_) SendMouseEvent(WebInputEvent::MouseMove, x, y); SendMouseEvent(WebInputEvent::MouseUp, x, y); mouse_pressed_ = false; } bool TouchStart(int x, int y, bool ack) { return SendTouchEvent( WebInputEvent::TouchStart, WebTouchPoint::StatePressed, x, y, ack); } bool TouchMove(int x, int y, bool ack) { return SendTouchEvent( WebInputEvent::TouchMove, WebTouchPoint::StateMoved, x, y, ack); } bool TouchEnd(int x, int y, bool ack) { return SendTouchEvent( WebInputEvent::TouchEnd, WebTouchPoint::StateReleased, x, y, ack); } WebTouchEvent MakeTouchEvent(WebInputEvent::Type type, WebTouchPoint::State state, int x, int y) { WebTouchEvent event; event.type = type; event.timeStampSeconds = GetNextEventTimeSeconds(); event.touchesLength = 1; event.touches[0].id = 0; event.touches[0].state = state; event.touches[0].position.x = x; event.touches[0].position.y = y; event.touches[0].screenPosition.x = x; event.touches[0].screenPosition.y = y; return event; } bool SendTouchEvent(WebInputEvent::Type type, WebTouchPoint::State state, int x, int y, bool ack) { WebTouchEvent event = MakeTouchEvent(type, state, x, y); if (emulator()->HandleTouchEvent(event)) { // Touch event is not forwarded. return false; } if (ack) { // Can't send ack if there are some pending acks. DCHECK(!touch_events_to_ack_.size()); // Touch event is forwarded, ack should not be handled by emulator. EXPECT_FALSE(emulator()->HandleTouchEventAck( event, INPUT_EVENT_ACK_STATE_CONSUMED)); } else { touch_events_to_ack_.push_back(event); } return true; } void AckOldestTouchEvent() { DCHECK(touch_events_to_ack_.size()); WebTouchEvent event = touch_events_to_ack_[0]; touch_events_to_ack_.erase(touch_events_to_ack_.begin()); // Emulator should not handle ack from native stream. EXPECT_FALSE(emulator()->HandleTouchEventAck( event, INPUT_EVENT_ACK_STATE_NO_CONSUMER_EXISTS)); } void DisableSynchronousTouchAck() { ack_touches_synchronously_ = false; } private: scoped_ptr<TouchEmulator> emulator_; std::vector<WebInputEvent::Type> forwarded_events_; #if defined(USE_AURA) scoped_ptr<gfx::Screen> screen_; #endif double last_event_time_seconds_; double event_time_delta_seconds_; bool shift_pressed_; bool mouse_pressed_; bool ack_touches_synchronously_; int last_mouse_x_; int last_mouse_y_; std::vector<WebTouchEvent> touch_events_to_ack_; base::MessageLoopForUI message_loop_; }; TEST_F(TouchEmulatorTest, NoTouches) { MouseMove(100, 200); MouseMove(300, 300); EXPECT_EQ("", ExpectedEvents()); } TEST_F(TouchEmulatorTest, Touch) { MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseUp(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate" " TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, DoubleTapSupport) { emulator()->SetDoubleTapSupportForPageEnabled(true); MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseUp(100, 200); EXPECT_EQ("TouchEnd GestureTapUnconfirmed", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapCancel GestureTapDown", ExpectedEvents()); MouseUp(100, 200); EXPECT_EQ("TouchEnd GestureTapCancel GestureDoubleTap", ExpectedEvents()); } TEST_F(TouchEmulatorTest, MultipleTouches) { MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseUp(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate" " TouchEnd GestureScrollEnd", ExpectedEvents()); MouseMove(300, 200); MouseMove(200, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(300, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(300, 300); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); MouseDrag(300, 400); EXPECT_EQ("TouchMove GestureScrollUpdate", ExpectedEvents()); MouseUp(300, 500); EXPECT_EQ( "TouchMove GestureScrollUpdate TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, Pinch) { MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); PressShift(); EXPECT_EQ("", ExpectedEvents()); MouseDrag(300, 200); EXPECT_EQ("TouchMove GesturePinchBegin", ExpectedEvents()); ReleaseShift(); EXPECT_EQ("", ExpectedEvents()); MouseDrag(400, 200); EXPECT_EQ( "TouchMove GesturePinchEnd GestureScrollUpdate", ExpectedEvents()); MouseUp(400, 200); EXPECT_EQ("TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, CancelWithDelayedAck) { DisableSynchronousTouchAck(); // Simulate a sequence that is interrupted by |CancelTouch()|. MouseDown(100, 200); EXPECT_EQ("TouchStart", ExpectedEvents()); MouseDrag(200, 200); EXPECT_EQ("TouchMove", ExpectedEvents()); emulator()->CancelTouch(); EXPECT_EQ("TouchCancel", ExpectedEvents()); // The mouse up should have no effect as the sequence was already cancelled. MouseUp(400, 200); EXPECT_EQ("", ExpectedEvents()); // Simulate a sequence that fully completes before |CancelTouch()|. MouseDown(100, 200); EXPECT_EQ("TouchStart", ExpectedEvents()); MouseUp(100, 200); EXPECT_EQ("TouchEnd", ExpectedEvents()); // |CancelTouch| should have no effect as the sequence was already terminated. emulator()->CancelTouch(); EXPECT_EQ("", ExpectedEvents()); } TEST_F(TouchEmulatorTest, DisableAndReenable) { MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); PressShift(); MouseDrag(300, 200); EXPECT_EQ("TouchMove GesturePinchBegin", ExpectedEvents()); // Disable while pinch is in progress. emulator()->Disable(); EXPECT_EQ("TouchCancel GesturePinchEnd GestureScrollEnd", ExpectedEvents()); MouseUp(300, 200); ReleaseShift(); MouseMove(300, 300); EXPECT_EQ("", ExpectedEvents()); emulator()->Enable(ui::GestureProviderConfigType::GENERIC_MOBILE); MouseDown(300, 300); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(300, 400); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); // Disable while scroll is in progress. emulator()->Disable(); EXPECT_EQ("TouchCancel GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, DisableAndReenableDifferentConfig) { MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); PressShift(); MouseDrag(300, 200); EXPECT_EQ("TouchMove GesturePinchBegin", ExpectedEvents()); // Disable while pinch is in progress. emulator()->Disable(); EXPECT_EQ("TouchCancel GesturePinchEnd GestureScrollEnd", ExpectedEvents()); MouseUp(300, 200); ReleaseShift(); MouseMove(300, 300); EXPECT_EQ("", ExpectedEvents()); emulator()->Enable(ui::GestureProviderConfigType::GENERIC_DESKTOP); MouseDown(300, 300); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseDrag(300, 400); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); // Disable while scroll is in progress. emulator()->Disable(); EXPECT_EQ("TouchCancel GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, MouseMovesDropped) { MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); // Mouse move after mouse down is never dropped. set_event_time_delta_seconds_(0.001); MouseDrag(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate", ExpectedEvents()); // The following mouse moves are dropped. MouseDrag(300, 200); EXPECT_EQ("", ExpectedEvents()); MouseDrag(350, 200); EXPECT_EQ("", ExpectedEvents()); // Dispatching again. set_event_time_delta_seconds_(0.1); MouseDrag(400, 200); EXPECT_EQ( "TouchMove GestureScrollUpdate", ExpectedEvents()); MouseUp(400, 200); EXPECT_EQ( "TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, MouseWheel) { MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); EXPECT_TRUE(SendMouseWheelEvent()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); EXPECT_FALSE(SendMouseWheelEvent()); MouseUp(100, 200); EXPECT_EQ("TouchEnd GestureShowPress GestureTap", ExpectedEvents()); EXPECT_TRUE(SendMouseWheelEvent()); MouseDown(300, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); EXPECT_FALSE(SendMouseWheelEvent()); emulator()->Disable(); EXPECT_EQ("TouchCancel GestureTapCancel", ExpectedEvents()); EXPECT_TRUE(SendMouseWheelEvent()); emulator()->Enable(ui::GestureProviderConfigType::GENERIC_MOBILE); EXPECT_TRUE(SendMouseWheelEvent()); } TEST_F(TouchEmulatorTest, MultipleTouchStreams) { // Native stream should be blocked while emulated is active. MouseMove(100, 200); EXPECT_EQ("", ExpectedEvents()); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); EXPECT_FALSE(TouchStart(10, 10, true)); EXPECT_FALSE(TouchMove(20, 20, true)); MouseUp(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate" " TouchEnd GestureScrollEnd", ExpectedEvents()); EXPECT_FALSE(TouchEnd(20, 20, true)); // Emulated stream should be blocked while native is active. EXPECT_TRUE(TouchStart(10, 10, true)); EXPECT_TRUE(TouchMove(20, 20, true)); MouseDown(300, 200); EXPECT_EQ("", ExpectedEvents()); // Re-enabling in the middle of a touch sequence should not affect this. emulator()->Disable(); emulator()->Enable(ui::GestureProviderConfigType::GENERIC_MOBILE); MouseDrag(300, 300); EXPECT_EQ("", ExpectedEvents()); MouseUp(300, 300); EXPECT_EQ("", ExpectedEvents()); EXPECT_TRUE(TouchEnd(20, 20, true)); EXPECT_EQ("", ExpectedEvents()); // Late ack for TouchEnd should not mess things up. EXPECT_TRUE(TouchStart(10, 10, false)); EXPECT_TRUE(TouchMove(20, 20, false)); emulator()->Disable(); EXPECT_TRUE(TouchEnd(20, 20, false)); EXPECT_TRUE(TouchStart(30, 30, false)); AckOldestTouchEvent(); // TouchStart. emulator()->Enable(ui::GestureProviderConfigType::GENERIC_MOBILE); AckOldestTouchEvent(); // TouchMove. AckOldestTouchEvent(); // TouchEnd. MouseDown(300, 200); EXPECT_EQ("", ExpectedEvents()); MouseDrag(300, 300); EXPECT_EQ("", ExpectedEvents()); MouseUp(300, 300); EXPECT_EQ("", ExpectedEvents()); AckOldestTouchEvent(); // TouchStart. MouseDown(300, 200); EXPECT_EQ("", ExpectedEvents()); EXPECT_TRUE(TouchMove(30, 40, true)); EXPECT_TRUE(TouchEnd(30, 40, true)); MouseUp(300, 200); EXPECT_EQ("", ExpectedEvents()); // Emulation should be back to normal. MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseUp(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate" " TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, MultipleTouchStreamsLateEnable) { // Enabling in the middle of native touch sequence should be handled. // Send artificial late TouchEnd ack, like it is the first thing emulator // does see. WebTouchEvent event = MakeTouchEvent( WebInputEvent::TouchEnd, WebTouchPoint::StateReleased, 10, 10); EXPECT_FALSE(emulator()->HandleTouchEventAck( event, INPUT_EVENT_ACK_STATE_CONSUMED)); MouseDown(100, 200); EXPECT_EQ("TouchStart GestureTapDown", ExpectedEvents()); MouseUp(200, 200); EXPECT_EQ( "TouchMove GestureTapCancel GestureScrollBegin GestureScrollUpdate" " TouchEnd GestureScrollEnd", ExpectedEvents()); } TEST_F(TouchEmulatorTest, CancelAfterDisableDoesNotCrash) { DisableSynchronousTouchAck(); MouseDown(100, 200); emulator()->Disable(); EXPECT_EQ("TouchStart TouchCancel", ExpectedEvents()); emulator()->CancelTouch(); } } // namespace content