chromium/ppapi/tests/test_ime_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_ime_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(ImeInputEvent);

namespace {

// Japanese Kanji letters
const char* kCompositionChar[] = {
    "\xE6\x96\x87",  // An example character of normal unicode.
    "\xF0\xA0\xAE\x9F", // An example character of surrogate pair.
    "\xF0\x9F\x98\x81"  // An example character of surrogate pair(emoji).
};

const char kCompositionText[] = "\xE6\x96\x87\xF0\xA0\xAE\x9F\xF0\x9F\x98\x81";

#define FINISHED_WAITING_MESSAGE "TEST_IME_INPUT_EVENT_FINISHED_WAITING"

}  // namespace

TestImeInputEvent::TestImeInputEvent(TestingInstance* instance)
    : TestCase(instance),
      input_event_interface_(NULL),
      keyboard_input_event_interface_(NULL),
      ime_input_event_interface_(NULL),
      received_unexpected_event_(true),
      received_finish_message_(false) {
}

TestImeInputEvent::~TestImeInputEvent() {
  // 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);
}

void TestImeInputEvent::RunTests(const std::string& filter) {
  RUN_TEST(ImeCommit, filter);
  RUN_TEST(ImeCancel, filter);
  RUN_TEST(ImeUnawareCommit, filter);
  RUN_TEST(ImeUnawareCancel, filter);
}

bool TestImeInputEvent::Init() {
  input_event_interface_ = static_cast<const PPB_InputEvent*>(
      pp::Module::Get()->GetBrowserInterface(PPB_INPUT_EVENT_INTERFACE));
  keyboard_input_event_interface_ =
      static_cast<const PPB_KeyboardInputEvent*>(
          pp::Module::Get()->GetBrowserInterface(
              PPB_KEYBOARD_INPUT_EVENT_INTERFACE));
  ime_input_event_interface_ = static_cast<const PPB_IMEInputEvent*>(
      pp::Module::Get()->GetBrowserInterface(
          PPB_IME_INPUT_EVENT_INTERFACE));

  bool success =
      input_event_interface_ &&
      keyboard_input_event_interface_ &&
      ime_input_event_interface_ &&
      CheckTestingInterface();

  // Set up a listener for our message that signals that all input events have
  // been received.
  // 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.
  std::string 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;
}

bool TestImeInputEvent::HandleInputEvent(const pp::InputEvent& input_event) {
  // Check whether the IME related events comes in the expected order.
  switch (input_event.GetType()) {
    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_START:
    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_UPDATE:
    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_END:
    case PP_INPUTEVENT_TYPE_IME_TEXT:
    case PP_INPUTEVENT_TYPE_CHAR:
      if (expected_events_.empty()) {
        received_unexpected_event_ = true;
      } else {
        received_unexpected_event_ =
          !AreEquivalentEvents(input_event.pp_resource(),
                               expected_events_.front().pp_resource());
        expected_events_.erase(expected_events_.begin());
      }
      break;

    default:
      // Don't care for any other input event types for this test.
      break;
  }

  // Handle all input events.
  return true;
}

void TestImeInputEvent::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;
  }
}

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

pp::InputEvent TestImeInputEvent::CreateImeCompositionStartEvent() {
  return pp::IMEInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_IME_COMPOSITION_START,
      100, // time_stamp
      pp::Var(""),
      std::vector<uint32_t>(),
      -1, // target_segment
      std::make_pair(0U, 0U) // selection
  );
}

pp::InputEvent TestImeInputEvent::CreateImeCompositionUpdateEvent(
    const std::string& text,
    const std::vector<uint32_t>& segments,
    int32_t target_segment,
    const std::pair<uint32_t, uint32_t>& selection) {
  return pp::IMEInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_IME_COMPOSITION_UPDATE,
      100, // time_stamp
      text,
      segments,
      target_segment,
      selection
  );
}

pp::InputEvent TestImeInputEvent::CreateImeCompositionEndEvent(
    const std::string& text) {
  return pp::IMEInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_IME_COMPOSITION_END,
      100, // time_stamp
      pp::Var(text),
      std::vector<uint32_t>(),
      -1, // target_segment
      std::make_pair(0U, 0U) // selection
  );
}

pp::InputEvent TestImeInputEvent::CreateImeTextEvent(const std::string& text) {
  return pp::IMEInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_IME_TEXT,
      100, // time_stamp
      pp::Var(text),
      std::vector<uint32_t>(),
      -1, // target_segment
      std::make_pair(0U, 0U) // selection
  );
}

pp::InputEvent TestImeInputEvent::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());
}

void TestImeInputEvent::GetFocusBySimulatingMouseClick() {
  // For receiving IME events, the plugin DOM node needs to be focused.
  // The following code is for achieving that by simulating a mouse click event.
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_MOUSE);
  SimulateInputEvent(pp::MouseInputEvent(
      instance_,
      PP_INPUTEVENT_TYPE_MOUSEDOWN,
      100,  // time_stamp
      0,  // modifiers
      PP_INPUTEVENT_MOUSEBUTTON_LEFT,
      pp::Point(
          view_rect_.x() + view_rect_.width() / 2,
          view_rect_.y() + view_rect_.height() / 2),
      1,  // click count
      pp::Point()));  // movement
}

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

bool TestImeInputEvent::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)
    return false;

  // Test event type-specific fields.
  switch (received_type) {
    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_START:
      // COMPOSITION_START does not convey further information.
      break;

    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_END:
    case PP_INPUTEVENT_TYPE_IME_TEXT:
      // For COMPOSITION_END and TEXT, GetText() has meaning.
      return pp::Var(pp::PASS_REF,
                     ime_input_event_interface_->GetText(received)) ==
             pp::Var(pp::PASS_REF,
                     ime_input_event_interface_->GetText(expected));

    case PP_INPUTEVENT_TYPE_IME_COMPOSITION_UPDATE:
      // For COMPOSITION_UPDATE, all fields must be checked.
      {
        uint32_t received_segment_number =
            ime_input_event_interface_->GetSegmentNumber(received);
        uint32_t expected_segment_number =
            ime_input_event_interface_->GetSegmentNumber(expected);
        if (received_segment_number != expected_segment_number)
          return false;

        // The "<=" is not a bug. i-th segment is represented as the pair of
        // i-th and (i+1)-th offsets in Pepper IME API.
        for (uint32_t i = 0; i <= received_segment_number; ++i) {
          if (ime_input_event_interface_->GetSegmentOffset(received, i) !=
              ime_input_event_interface_->GetSegmentOffset(expected, i))
            return false;
        }

        uint32_t received_selection_start = 0;
        uint32_t received_selection_end = 0;
        uint32_t expected_selection_start = 0;
        uint32_t expected_selection_end = 0;
        ime_input_event_interface_->GetSelection(
            received, &received_selection_start, &received_selection_end);
        ime_input_event_interface_->GetSelection(
            expected, &expected_selection_start, &expected_selection_end);
        if (received_selection_start != expected_selection_start ||
            received_selection_end != expected_selection_end) {
          return true;
        }

        return pp::Var(pp::PASS_REF,
                       ime_input_event_interface_->GetText(received)) ==
               pp::Var(pp::PASS_REF,
                       ime_input_event_interface_->GetText(expected)) &&
               ime_input_event_interface_->GetTargetSegment(received) ==
               ime_input_event_interface_->GetTargetSegment(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));

    default:
      break;
  }
  return true;
}

std::string TestImeInputEvent::TestImeCommit() {
  GetFocusBySimulatingMouseClick();

  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_KEYBOARD |
                                             PP_INPUTEVENT_CLASS_IME);

  std::vector<uint32_t> segments;
  segments.push_back(0U);
  segments.push_back(3U);
  segments.push_back(7U);
  segments.push_back(11U);
  pp::InputEvent update_event = CreateImeCompositionUpdateEvent(
      kCompositionText, segments, 1, std::make_pair(3U, 7U));

  expected_events_.clear();
  expected_events_.push_back(CreateImeCompositionStartEvent());
  expected_events_.push_back(update_event);
  expected_events_.push_back(CreateImeCompositionEndEvent(kCompositionText));
  expected_events_.push_back(CreateImeTextEvent(kCompositionText));

  // Simulate the case when IME successfully committed some text.
  ASSERT_TRUE(SimulateInputEvent(update_event));
  ASSERT_TRUE(SimulateInputEvent(CreateImeTextEvent(kCompositionText)));

  ASSERT_TRUE(expected_events_.empty());
  PASS();
}

std::string TestImeInputEvent::TestImeCancel() {
  GetFocusBySimulatingMouseClick();

  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_KEYBOARD |
                                             PP_INPUTEVENT_CLASS_IME);

  std::vector<uint32_t> segments;
  segments.push_back(0U);
  segments.push_back(3U);
  segments.push_back(7U);
  segments.push_back(11U);
  pp::InputEvent update_event = CreateImeCompositionUpdateEvent(
      kCompositionText, segments, 1, std::make_pair(3U, 7U));

  expected_events_.clear();
  expected_events_.push_back(CreateImeCompositionStartEvent());
  expected_events_.push_back(update_event);
  expected_events_.push_back(CreateImeCompositionEndEvent(std::string()));

  // Simulate the case when IME canceled composition.
  ASSERT_TRUE(SimulateInputEvent(update_event));
  ASSERT_TRUE(SimulateInputEvent(CreateImeCompositionEndEvent(std::string())));

  ASSERT_TRUE(expected_events_.empty());
  PASS();
}

std::string TestImeInputEvent::TestImeUnawareCommit() {
  GetFocusBySimulatingMouseClick();

  input_event_interface_->ClearInputEventRequest(instance_->pp_instance(),
                                                 PP_INPUTEVENT_CLASS_IME);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_KEYBOARD);

  std::vector<uint32_t> segments;
  segments.push_back(0U);
  segments.push_back(3U);
  segments.push_back(7U);
  segments.push_back(11U);
  pp::InputEvent update_event = CreateImeCompositionUpdateEvent(
      kCompositionText, segments, 1, std::make_pair(3U, 7U));

  expected_events_.clear();
  expected_events_.push_back(CreateCharEvent(kCompositionChar[0]));
  expected_events_.push_back(CreateCharEvent(kCompositionChar[1]));
  expected_events_.push_back(CreateCharEvent(kCompositionChar[2]));

  // Test for IME-unaware plugins. Commit event is translated to char events.
  ASSERT_TRUE(SimulateInputEvent(update_event));
  ASSERT_TRUE(SimulateInputEvent(CreateImeTextEvent(kCompositionText)));

  ASSERT_TRUE(expected_events_.empty());
  PASS();
}


std::string TestImeInputEvent::TestImeUnawareCancel() {
  GetFocusBySimulatingMouseClick();

  input_event_interface_->ClearInputEventRequest(instance_->pp_instance(),
                                                 PP_INPUTEVENT_CLASS_IME);
  input_event_interface_->RequestInputEvents(instance_->pp_instance(),
                                             PP_INPUTEVENT_CLASS_KEYBOARD);

  std::vector<uint32_t> segments;
  segments.push_back(0U);
  segments.push_back(3U);
  segments.push_back(7U);
  segments.push_back(11U);
  pp::InputEvent update_event = CreateImeCompositionUpdateEvent(
      kCompositionText, segments, 1, std::make_pair(3U, 7U));

  expected_events_.clear();

  // Test for IME-unaware plugins. Cancel won't issue any events.
  ASSERT_TRUE(SimulateInputEvent(update_event));
  ASSERT_TRUE(SimulateInputEvent(CreateImeCompositionEndEvent(std::string())));

  ASSERT_TRUE(expected_events_.empty());
  PASS();
}