chromium/ppapi/tests/test_input_event.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ppapi/tests/test_input_event.h"

#include "ppapi/c/pp_errors.h"
#include "ppapi/c/ppb_input_event.h"
#include "ppapi/cpp/input_event.h"
#include "ppapi/cpp/module.h"
#include "ppapi/tests/test_utils.h"
#include "ppapi/tests/testing_instance.h"

REGISTER_TEST_CASE(InputEvent);

namespace {

const uint32_t kSpaceChar = 0x20;
const char* kSpaceString = " ";
const char* kSpaceCode = "Space";

#define FINISHED_WAITING_MESSAGE "TEST_INPUT_EVENT_FINISHED_WAITING"

pp::Point GetCenter(const pp::Rect& rect) {
  return pp::Point(
      rect.x() + rect.width() / 2,
      rect.y() + rect.height() / 2);
}

}  // namespace

void TestInputEvent::RunTests(const std::string& filter) {
  RUN_TEST(Events, filter);

  // The AcceptTouchEvent_N tests should not be run when the filter is empty;
  // they can only be run one at a time.
  // TODO(dmichael): Figure out a way to make these run in the same test fixture
  //                 instance.
  if (!ShouldRunAllTests(filter)) {
    RUN_TEST(AcceptTouchEvent_1, filter);
    RUN_TEST(AcceptTouchEvent_2, filter);
    RUN_TEST(AcceptTouchEvent_3, filter);
    RUN_TEST(AcceptTouchEvent_4, filter);
  }
}

TestInputEvent::TestInputEvent(TestingInstance* instance)
    : TestCase(instance),
      input_event_interface_(NULL),
      mouse_input_event_interface_(NULL),
      wheel_input_event_interface_(NULL),
      keyboard_input_event_interface_(NULL),
      touch_input_event_interface_(NULL),
      nested_event_(instance->pp_instance()),
      view_rect_(),
      expected_input_event_(0),
      received_expected_event_(false),
      received_finish_message_(false) {
}

TestInputEvent::~TestInputEvent() {
  // Remove the special listener that only responds to a
  // FINISHED_WAITING_MESSAGE string. See Init for where it gets added.
  std::string js_code;
  js_code += "var plugin = document.getElementById('plugin');"
             "plugin.removeEventListener('message',"
             "                           plugin.wait_for_messages_handler);"
             "delete plugin.wait_for_messages_handler;";
  instance_->EvalScript(js_code);
}

bool TestInputEvent::Init() {
  input_event_interface_ = static_cast<const PPB_InputEvent*>(
      pp::Module::Get()->GetBrowserInterface(PPB_INPUT_EVENT_INTERFACE));
  mouse_input_event_interface_ = static_cast<const PPB_MouseInputEvent*>(
      pp::Module::Get()->GetBrowserInterface(
          PPB_MOUSE_INPUT_EVENT_INTERFACE));
  wheel_input_event_interface_ = static_cast<const PPB_WheelInputEvent*>(
      pp::Module::Get()->GetBrowserInterface(
          PPB_WHEEL_INPUT_EVENT_INTERFACE));
  keyboard_input_event_interface_ = static_cast<const PPB_KeyboardInputEvent*>(
      pp::Module::Get()->GetBrowserInterface(
          PPB_KEYBOARD_INPUT_EVENT_INTERFACE));
  touch_input_event_interface_ = static_cast<const PPB_TouchInputEvent*>(
      pp::Module::Get()->GetBrowserInterface(
          PPB_TOUCH_INPUT_EVENT_INTERFACE));

  bool success =
      input_event_interface_ &&
      mouse_input_event_interface_ &&
      wheel_input_event_interface_ &&
      keyboard_input_event_interface_ &&
      touch_input_event_interface_ &&
      CheckTestingInterface();

  // Set up a listener for our message that signals that all input events have
  // been received.
  std::string js_code;
  // Note the following code is dependent on some features of test_case.html.
  // E.g., it is assumed that the DOM element where the plugin is embedded has
  // an id of 'plugin', and there is a function 'IsTestingMessage' that allows
  // us to ignore the messages that are intended for use by the testing
  // framework itself.
  js_code += "var plugin = document.getElementById('plugin');"
             "var wait_for_messages_handler = function(message_event) {"
             "  if (!IsTestingMessage(message_event.data) &&"
             "      message_event.data === '" FINISHED_WAITING_MESSAGE "') {"
             "    plugin.postMessage('" FINISHED_WAITING_MESSAGE "');"
             "  }"
             "};"
             "plugin.addEventListener('message', wait_for_messages_handler);"
             // Stash it on the plugin so we can remove it in the destructor.
             "plugin.wait_for_messages_handler = wait_for_messages_handler;";
  instance_->EvalScript(js_code);

  return success;
}

pp::InputEvent TestInputEvent::CreateMouseEvent(
    PP_InputEvent_Type type,
    PP_InputEvent_MouseButton buttons) {
  return pp::MouseInputEvent(
      instance_,
      type,
      100,  // time_stamp
      0,  // modifiers
      buttons,
      GetCenter(view_rect_),
      1,  // click count
      pp::Point());  // movement
}

pp::InputEvent TestInputEvent::CreateWheelEvent() {
  return pp::WheelInputEvent(
      instance_,
      100,  // time_stamp
      0,  // modifiers
      pp::FloatPoint(1, 2),
      pp::FloatPoint(3, 4),
      PP_TRUE);  // scroll_by_page
}

pp::InputEvent TestInputEvent::CreateKeyEvent(PP_InputEvent_Type type,
                                              uint32_t key_code,
                                              const std::string& code) {
  return pp::KeyboardInputEvent(
      instance_,
      type,
      100,  // time_stamp
      0,  // modifiers
      key_code,
      pp::Var(),
      pp::Var(code));
}

pp::InputEvent TestInputEvent::CreateCharEvent(const std::string& text) {
  return pp::KeyboardInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_CHAR,
      100,  // time_stamp
      0,  // modifiers
      0,  // keycode
      pp::Var(text),
      pp::Var());
}

pp::InputEvent TestInputEvent::CreateTouchEvent(PP_InputEvent_Type type,
                                                const pp::FloatPoint& point) {
  PP_TouchPoint touch_point = PP_MakeTouchPoint();
  touch_point.position = point;

  pp::TouchInputEvent touch_event(instance_, type, 100, 0);
  touch_event.AddTouchPoint(PP_TOUCHLIST_TYPE_TOUCHES, touch_point);
  touch_event.AddTouchPoint(PP_TOUCHLIST_TYPE_CHANGEDTOUCHES, touch_point);
  touch_event.AddTouchPoint(PP_TOUCHLIST_TYPE_TARGETTOUCHES, touch_point);

  return touch_event;
}

void TestInputEvent::PostMessageBarrier() {
  received_finish_message_ = false;
  instance_->PostMessage(pp::Var(FINISHED_WAITING_MESSAGE));
  testing_interface_->RunMessageLoop(instance_->pp_instance());
  nested_event_.Wait();
}

// Simulates the input event and calls PostMessage to let us know when
// we have received all resulting events from the browser.
bool TestInputEvent::SimulateInputEvent(
    const pp::InputEvent& input_event) {
  expected_input_event_ = pp::InputEvent(input_event.pp_resource());
  received_expected_event_ = false;
  testing_interface_->SimulateInputEvent(instance_->pp_instance(),
                                         input_event.pp_resource());
  PostMessageBarrier();
  return received_expected_event_;
}

bool TestInputEvent::AreEquivalentEvents(PP_Resource received,
                                         PP_Resource expected) {
  if (!input_event_interface_->IsInputEvent(received) ||
      !input_event_interface_->IsInputEvent(expected)) {
    return false;
  }

  // Test common fields, except modifiers and time stamp, which may be changed
  // by the browser.
  int32_t received_type = input_event_interface_->GetType(received);
  int32_t expected_type = input_event_interface_->GetType(expected);
  if (received_type != expected_type) {
    // Allow key down events to match "raw" key down events.
    if (expected_type != PP_INPUTEVENT_TYPE_KEYDOWN &&
        received_type != PP_INPUTEVENT_TYPE_RAWKEYDOWN) {
      return false;
    }
  }

  // Test event type-specific fields.
  switch (input_event_interface_->GetType(received)) {
    case PP_INPUTEVENT_TYPE_MOUSEDOWN:
    case PP_INPUTEVENT_TYPE_MOUSEUP:
    case PP_INPUTEVENT_TYPE_MOUSEMOVE:
    case PP_INPUTEVENT_TYPE_MOUSEENTER:
    case PP_INPUTEVENT_TYPE_MOUSELEAVE:
      // Check mouse fields, except position and movement, which may be
      // modified by the renderer.
      return
          mouse_input_event_interface_->GetButton(received) ==
          mouse_input_event_interface_->GetButton(expected) &&
          mouse_input_event_interface_->GetClickCount(received) ==
          mouse_input_event_interface_->GetClickCount(expected);

    case PP_INPUTEVENT_TYPE_WHEEL:
      return
          pp::FloatPoint(wheel_input_event_interface_->GetDelta(received)) ==
          pp::FloatPoint(wheel_input_event_interface_->GetDelta(expected)) &&
          pp::FloatPoint(wheel_input_event_interface_->GetTicks(received)) ==
          pp::FloatPoint(wheel_input_event_interface_->GetTicks(expected)) &&
          wheel_input_event_interface_->GetScrollByPage(received) ==
          wheel_input_event_interface_->GetScrollByPage(expected);

    case PP_INPUTEVENT_TYPE_RAWKEYDOWN:
    case PP_INPUTEVENT_TYPE_KEYDOWN:
    case PP_INPUTEVENT_TYPE_KEYUP:
      return
          keyboard_input_event_interface_->GetKeyCode(received) ==
          keyboard_input_event_interface_->GetKeyCode(expected);

    case PP_INPUTEVENT_TYPE_CHAR:
      return
          keyboard_input_event_interface_->GetKeyCode(received) ==
          keyboard_input_event_interface_->GetKeyCode(expected) &&
          pp::Var(pp::PASS_REF,
              keyboard_input_event_interface_->GetCharacterText(received)) ==
          pp::Var(pp::PASS_REF,
              keyboard_input_event_interface_->GetCharacterText(expected));

    case PP_INPUTEVENT_TYPE_TOUCHSTART:
    case PP_INPUTEVENT_TYPE_TOUCHMOVE:
    case PP_INPUTEVENT_TYPE_TOUCHEND:
    case PP_INPUTEVENT_TYPE_TOUCHCANCEL: {
      if (!touch_input_event_interface_->IsTouchInputEvent(received) ||
          !touch_input_event_interface_->IsTouchInputEvent(expected))
        return false;

      uint32_t touch_count = touch_input_event_interface_->GetTouchCount(
          received, PP_TOUCHLIST_TYPE_TOUCHES);
      if (touch_count <= 0 ||
          touch_count != touch_input_event_interface_->GetTouchCount(expected,
              PP_TOUCHLIST_TYPE_TOUCHES))
        return false;

      for (uint32_t i = 0; i < touch_count; ++i) {
        PP_TouchPoint expected_point = touch_input_event_interface_->
            GetTouchByIndex(expected, PP_TOUCHLIST_TYPE_TOUCHES, i);
        PP_TouchPoint received_point = touch_input_event_interface_->
            GetTouchByIndex(received, PP_TOUCHLIST_TYPE_TOUCHES, i);

        if (expected_point.id != received_point.id ||
            expected_point.radius != received_point.radius ||
            expected_point.rotation_angle != received_point.rotation_angle ||
            expected_point.pressure != received_point.pressure)
          return false;

        if (expected_point.position.x != received_point.position.x ||
            expected_point.position.y != received_point.position.y)
          return false;
      }
      return true;
    }

    default:
      break;
  }

  return false;
}

bool TestInputEvent::HandleInputEvent(const pp::InputEvent& input_event) {
  // Some events may cause extra events to be generated, so look for the
  // first one that matches.
  if (!received_expected_event_) {
    received_expected_event_ = AreEquivalentEvents(
        input_event.pp_resource(),
        expected_input_event_.pp_resource());
  }
  // Handle all input events.
  return true;
}

void TestInputEvent::HandleMessage(const pp::Var& message_data) {
  if (message_data.is_string() &&
      (message_data.AsString() == FINISHED_WAITING_MESSAGE)) {
    testing_interface_->QuitMessageLoop(instance_->pp_instance());
    received_finish_message_ = true;
    nested_event_.Signal();
  }
}

void TestInputEvent::DidChangeView(const pp::View& view) {
  view_rect_ = view.GetRect();
}

std::string TestInputEvent::TestEvents() {
  // Request all input event classes.
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE |
                                             PP_INPUTEVENT_CLASS_WHEEL |
                                             PP_INPUTEVENT_CLASS_KEYBOARD |
                                             PP_INPUTEVENT_CLASS_TOUCH);
  PostMessageBarrier();

  // Send the events and check that we received them.
  ASSERT_TRUE(
      SimulateInputEvent(CreateMouseEvent(PP_INPUTEVENT_TYPE_MOUSEDOWN,
                                          PP_INPUTEVENT_MOUSEBUTTON_LEFT)));
  ASSERT_TRUE(
      SimulateInputEvent(CreateWheelEvent()));
  ASSERT_TRUE(
      SimulateInputEvent(CreateKeyEvent(PP_INPUTEVENT_TYPE_KEYDOWN,
                                        kSpaceChar, kSpaceCode)));
  ASSERT_TRUE(
      SimulateInputEvent(CreateCharEvent(kSpaceString)));
  ASSERT_TRUE(SimulateInputEvent(CreateTouchEvent(PP_INPUTEVENT_TYPE_TOUCHSTART,
                                                  pp::FloatPoint(12, 23))));
  // Request only mouse events.
  input_event_interface_->ClearInputEventRequest(instance_->pp_instance(),
                                                 PP_INPUTEVENT_CLASS_WHEEL |
                                                 PP_INPUTEVENT_CLASS_KEYBOARD);
  PostMessageBarrier();

  // Check that we only receive mouse events.
  ASSERT_TRUE(
      SimulateInputEvent(CreateMouseEvent(PP_INPUTEVENT_TYPE_MOUSEDOWN,
                                          PP_INPUTEVENT_MOUSEBUTTON_LEFT)));
  ASSERT_FALSE(
      SimulateInputEvent(CreateWheelEvent()));
  ASSERT_FALSE(
      SimulateInputEvent(CreateKeyEvent(PP_INPUTEVENT_TYPE_KEYDOWN,
                                        kSpaceChar, kSpaceCode)));
  ASSERT_FALSE(
      SimulateInputEvent(CreateCharEvent(kSpaceString)));

  PASS();
}

std::string TestInputEvent::TestAcceptTouchEvent_1() {
  // The browser normally sends touch-events to the renderer only if the page
  // has touch-event handlers. Since test-case.html does not have any
  // touch-event handler, it would normally not receive any touch events from
  // the browser. However, if a plugin in the page does accept touch events,
  // then the browser should start sending touch-events to the page. In this
  // test, the plugin simply registers for touch-events. The real test is to
  // verify that the browser knows to send touch-events to the renderer.
  // If the plugin is removed from the page, then there are no more touch-event
  // handlers in the page, and browser stops sending touch-events. So to make
  // it possible to test this properly, the plugin is not removed from the page
  // at the end of the test.
  instance_->set_remove_plugin(false);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE |
                                             PP_INPUTEVENT_CLASS_WHEEL |
                                             PP_INPUTEVENT_CLASS_KEYBOARD |
                                             PP_INPUTEVENT_CLASS_TOUCH);
  PASS();
}

std::string TestInputEvent::TestAcceptTouchEvent_2() {
  // See comment in TestAcceptTouchEvent_1.
  instance_->set_remove_plugin(false);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE |
                                             PP_INPUTEVENT_CLASS_WHEEL |
                                             PP_INPUTEVENT_CLASS_KEYBOARD |
                                             PP_INPUTEVENT_CLASS_TOUCH);
  input_event_interface_->ClearInputEventRequest(instance_->pp_instance(),
                                                 PP_INPUTEVENT_CLASS_TOUCH);
  PASS();
}

std::string TestInputEvent::TestAcceptTouchEvent_3() {
  // See comment in TestAcceptTouchEvent_1.
  instance_->set_remove_plugin(false);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE |
                                             PP_INPUTEVENT_CLASS_WHEEL |
                                             PP_INPUTEVENT_CLASS_KEYBOARD);
  input_event_interface_->RequestFilteringInputEvents(instance_->pp_instance(),
      PP_INPUTEVENT_CLASS_TOUCH);
  PASS();
}

std::string TestInputEvent::TestAcceptTouchEvent_4() {
  // See comment in TestAcceptTouchEvent_1.
  instance_->set_remove_plugin(false);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE |
                                             PP_INPUTEVENT_CLASS_WHEEL |
                                             PP_INPUTEVENT_CLASS_KEYBOARD);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_TOUCH);
  PASS();
}