blob: 0d41845bbc8dee65ee09717f7837aa4703645ae6 [file] [log] [blame]
// 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 "core/editing/InputMethodController.h"
#include "core/dom/Document.h"
#include "core/dom/Element.h"
#include "core/dom/Range.h"
#include "core/editing/FrameSelection.h"
#include "core/events/MouseEvent.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLInputElement.h"
#include "core/testing/DummyPageHolder.h"
#include "testing/gtest/include/gtest/gtest.h"
#include <memory>
namespace blink {
class InputMethodControllerTest : public ::testing::Test {
protected:
InputMethodController& controller() { return frame().inputMethodController(); }
Document& document() const { return *m_document; }
LocalFrame& frame() const { return m_dummyPageHolder->frame(); }
Element* insertHTMLElement(const char* elementCode, const char* elementId);
private:
void SetUp() override;
std::unique_ptr<DummyPageHolder> m_dummyPageHolder;
Persistent<Document> m_document;
};
void InputMethodControllerTest::SetUp()
{
m_dummyPageHolder = DummyPageHolder::create(IntSize(800, 600));
m_document = &m_dummyPageHolder->document();
DCHECK(m_document);
}
Element* InputMethodControllerTest::insertHTMLElement(
const char* elementCode, const char* elementId)
{
document().write(elementCode);
document().updateStyleAndLayout();
Element* element = document().getElementById(elementId);
element->focus();
return element;
}
TEST_F(InputMethodControllerTest, BackspaceFromEndOfInput)
{
HTMLInputElement* input = toHTMLInputElement(
insertHTMLElement("<input id='sample'>", "sample"));
input->setValue("fooX");
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("fooX", input->value().utf8().data());
controller().extendSelectionAndDelete(0, 0);
EXPECT_STREQ("fooX", input->value().utf8().data());
input->setValue("fooX");
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("fooX", input->value().utf8().data());
controller().extendSelectionAndDelete(1, 0);
EXPECT_STREQ("foo", input->value().utf8().data());
input->setValue(String::fromUTF8("foo\xE2\x98\x85")); // U+2605 == "black star"
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("foo\xE2\x98\x85", input->value().utf8().data());
controller().extendSelectionAndDelete(1, 0);
EXPECT_STREQ("foo", input->value().utf8().data());
input->setValue(String::fromUTF8("foo\xF0\x9F\x8F\x86")); // U+1F3C6 == "trophy"
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("foo\xF0\x9F\x8F\x86", input->value().utf8().data());
controller().extendSelectionAndDelete(1, 0);
EXPECT_STREQ("foo", input->value().utf8().data());
input->setValue(String::fromUTF8("foo\xE0\xB8\x81\xE0\xB9\x89")); // composed U+0E01 "ka kai" + U+0E49 "mai tho"
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("foo\xE0\xB8\x81\xE0\xB9\x89", input->value().utf8().data());
controller().extendSelectionAndDelete(1, 0);
EXPECT_STREQ("foo", input->value().utf8().data());
input->setValue("fooX");
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("fooX", input->value().utf8().data());
controller().extendSelectionAndDelete(0, 1);
EXPECT_STREQ("fooX", input->value().utf8().data());
}
TEST_F(InputMethodControllerTest, SetCompositionFromExistingText)
{
Element* div = insertHTMLElement(
"<div id='sample' contenteditable='true'>hello world</div>", "sample");
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
controller().setCompositionFromExistingText(underlines, 0, 5);
Range* range = controller().compositionRange();
EXPECT_EQ(0, range->startOffset());
EXPECT_EQ(5, range->endOffset());
PlainTextRange plainTextRange(PlainTextRange::create(*div, *range));
EXPECT_EQ(0u, plainTextRange.start());
EXPECT_EQ(5u, plainTextRange.end());
}
TEST_F(InputMethodControllerTest, SelectionOnConfirmExistingText)
{
insertHTMLElement(
"<div id='sample' contenteditable='true'>hello world</div>", "sample");
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
controller().setCompositionFromExistingText(underlines, 0, 5);
controller().confirmComposition();
EXPECT_EQ(0, frame().selection().start().computeOffsetInContainerNode());
EXPECT_EQ(0, frame().selection().end().computeOffsetInContainerNode());
}
TEST_F(InputMethodControllerTest, DeleteBySettingEmptyComposition)
{
HTMLInputElement* input = toHTMLInputElement(
insertHTMLElement("<input id='sample'>", "sample"));
input->setValue("foo ");
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("foo ", input->value().utf8().data());
controller().extendSelectionAndDelete(0, 0);
EXPECT_STREQ("foo ", input->value().utf8().data());
input->setValue("foo ");
controller().setEditableSelectionOffsets(PlainTextRange(4, 4));
EXPECT_STREQ("foo ", input->value().utf8().data());
controller().extendSelectionAndDelete(1, 0);
EXPECT_STREQ("foo", input->value().utf8().data());
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 3, Color(255, 0, 0), false, 0));
controller().setCompositionFromExistingText(underlines, 0, 3);
controller().setComposition(String(""), underlines, 0, 3);
EXPECT_STREQ("", input->value().utf8().data());
}
TEST_F(InputMethodControllerTest, SetCompositionFromExistingTextWithCollapsedWhiteSpace)
{
// Creates a div with one leading new line char. The new line char is hidden
// from the user and IME, but is visible to InputMethodController.
Element* div = insertHTMLElement(
"<div id='sample' contenteditable='true'>\nhello world</div>", "sample");
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
controller().setCompositionFromExistingText(underlines, 0, 5);
Range* range = controller().compositionRange();
EXPECT_EQ(1, range->startOffset());
EXPECT_EQ(6, range->endOffset());
PlainTextRange plainTextRange(PlainTextRange::create(*div, *range));
EXPECT_EQ(0u, plainTextRange.start());
EXPECT_EQ(5u, plainTextRange.end());
}
TEST_F(InputMethodControllerTest, SetCompositionFromExistingTextWithInvalidOffsets)
{
insertHTMLElement("<div id='sample' contenteditable='true'>test</div>", "sample");
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(7, 8, Color(255, 0, 0), false, 0));
controller().setCompositionFromExistingText(underlines, 7, 8);
EXPECT_FALSE(controller().compositionRange());
}
TEST_F(InputMethodControllerTest, ConfirmPasswordComposition)
{
HTMLInputElement* input = toHTMLInputElement(
insertHTMLElement("<input id='sample' type='password' size='24'>", "sample"));
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
controller().setComposition("foo", underlines, 0, 3);
controller().confirmComposition();
EXPECT_STREQ("foo", input->value().utf8().data());
}
TEST_F(InputMethodControllerTest, SetCompositionForInputWithDifferentNewCursorPositions)
{
HTMLInputElement* input = toHTMLInputElement(
insertHTMLElement("<input id='sample'>", "sample"));
input->setValue("hello");
controller().setEditableSelectionOffsets(PlainTextRange(2, 2));
EXPECT_STREQ("hello", input->value().utf8().data());
EXPECT_EQ(2u, controller().getSelectionOffsets().start());
EXPECT_EQ(2u, controller().getSelectionOffsets().end());
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 2, Color(255, 0, 0), false, 0));
// The cursor exceeds left boundary.
// "*heABllo", where * stands for cursor.
controller().setComposition("AB", underlines, -100, -100);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(0u, controller().getSelectionOffsets().start());
EXPECT_EQ(0u, controller().getSelectionOffsets().end());
// The cursor is on left boundary.
// "*heABllo".
controller().setComposition("AB", underlines, -2, -2);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(0u, controller().getSelectionOffsets().start());
EXPECT_EQ(0u, controller().getSelectionOffsets().end());
// The cursor is before the composing text.
// "he*ABllo".
controller().setComposition("AB", underlines, 0, 0);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(2u, controller().getSelectionOffsets().start());
EXPECT_EQ(2u, controller().getSelectionOffsets().end());
// The cursor is after the composing text.
// "heAB*llo".
controller().setComposition("AB", underlines, 2, 2);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(4u, controller().getSelectionOffsets().start());
EXPECT_EQ(4u, controller().getSelectionOffsets().end());
// The cursor is on right boundary.
// "heABllo*".
controller().setComposition("AB", underlines, 5, 5);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(7u, controller().getSelectionOffsets().start());
EXPECT_EQ(7u, controller().getSelectionOffsets().end());
// The cursor exceeds right boundary.
// "heABllo*".
controller().setComposition("AB", underlines, 100, 100);
EXPECT_STREQ("heABllo", input->value().utf8().data());
EXPECT_EQ(7u, controller().getSelectionOffsets().start());
EXPECT_EQ(7u, controller().getSelectionOffsets().end());
}
TEST_F(InputMethodControllerTest, SetCompositionForContentEditableWithDifferentNewCursorPositions)
{
// There are 7 nodes and 5+1+5+1+3+4+3 characters: "hello", '\n', "world", "\n", "012", "3456", "789".
Element* div = insertHTMLElement(
"<div id='sample' contenteditable='true'>"
"hello"
"<div id='sample2' contenteditable='true'>world"
"<p>012<b>3456</b><i>789</i></p>"
"</div>"
"</div>",
"sample");
controller().setEditableSelectionOffsets(PlainTextRange(17, 17));
EXPECT_STREQ("hello\nworld\n0123456789", div->innerText().utf8().data());
EXPECT_EQ(17u, controller().getSelectionOffsets().start());
EXPECT_EQ(17u, controller().getSelectionOffsets().end());
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 2, Color(255, 0, 0), false, 0));
// The cursor exceeds left boundary.
// "*hello\nworld\n01234AB56789", where * stands for cursor.
controller().setComposition("AB", underlines, -100, -100);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(0u, controller().getSelectionOffsets().start());
EXPECT_EQ(0u, controller().getSelectionOffsets().end());
// The cursor is on left boundary.
// "*hello\nworld\n01234AB56789".
controller().setComposition("AB", underlines, -17, -17);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(0u, controller().getSelectionOffsets().start());
EXPECT_EQ(0u, controller().getSelectionOffsets().end());
// The cursor is in the 1st node.
// "he*llo\nworld\n01234AB56789".
controller().setComposition("AB", underlines, -15, -15);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(2u, controller().getSelectionOffsets().start());
EXPECT_EQ(2u, controller().getSelectionOffsets().end());
// The cursor is on right boundary of the 1st node.
// "hello*\nworld\n01234AB56789".
controller().setComposition("AB", underlines, -12, -12);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(5u, controller().getSelectionOffsets().start());
EXPECT_EQ(5u, controller().getSelectionOffsets().end());
// The cursor is on right boundary of the 2nd node.
// "hello\n*world\n01234AB56789".
controller().setComposition("AB", underlines, -11, -11);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(6u, controller().getSelectionOffsets().start());
EXPECT_EQ(6u, controller().getSelectionOffsets().end());
// The cursor is on right boundary of the 3rd node.
// "hello\nworld*\n01234AB56789".
controller().setComposition("AB", underlines, -6, -6);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(11u, controller().getSelectionOffsets().start());
EXPECT_EQ(11u, controller().getSelectionOffsets().end());
// The cursor is on right boundary of the 4th node.
// "hello\nworld\n*01234AB56789".
controller().setComposition("AB", underlines, -5, -5);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(12u, controller().getSelectionOffsets().start());
EXPECT_EQ(12u, controller().getSelectionOffsets().end());
// The cursor is before the composing text.
// "hello\nworld\n01234*AB56789".
controller().setComposition("AB", underlines, 0, 0);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(17u, controller().getSelectionOffsets().start());
EXPECT_EQ(17u, controller().getSelectionOffsets().end());
// The cursor is after the composing text.
// "hello\nworld\n01234AB*56789".
controller().setComposition("AB", underlines, 2, 2);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(19u, controller().getSelectionOffsets().start());
EXPECT_EQ(19u, controller().getSelectionOffsets().end());
// The cursor is on right boundary.
// "hello\nworld\n01234AB56789*".
controller().setComposition("AB", underlines, 7, 7);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(24u, controller().getSelectionOffsets().start());
EXPECT_EQ(24u, controller().getSelectionOffsets().end());
// The cursor exceeds right boundary.
// "hello\nworld\n01234AB56789*".
controller().setComposition("AB", underlines, 100, 100);
EXPECT_STREQ("hello\nworld\n01234AB56789", div->innerText().utf8().data());
EXPECT_EQ(24u, controller().getSelectionOffsets().start());
EXPECT_EQ(24u, controller().getSelectionOffsets().end());
}
TEST_F(InputMethodControllerTest, SetCompositionWithEmptyText)
{
Element* div = insertHTMLElement(
"<div id='sample' contenteditable='true'>hello</div>",
"sample");
controller().setEditableSelectionOffsets(PlainTextRange(2, 2));
EXPECT_STREQ("hello", div->innerText().utf8().data());
EXPECT_EQ(2u, controller().getSelectionOffsets().start());
EXPECT_EQ(2u, controller().getSelectionOffsets().end());
Vector<CompositionUnderline> underlines0;
underlines0.append(CompositionUnderline(0, 0, Color(255, 0, 0), false, 0));
Vector<CompositionUnderline> underlines2;
underlines2.append(CompositionUnderline(0, 2, Color(255, 0, 0), false, 0));
controller().setComposition("AB", underlines2, 2, 2);
// With previous composition.
controller().setComposition("", underlines0, 2, 2);
EXPECT_STREQ("hello", div->innerText().utf8().data());
EXPECT_EQ(4u, controller().getSelectionOffsets().start());
EXPECT_EQ(4u, controller().getSelectionOffsets().end());
// Without previous composition.
controller().setComposition("", underlines0, -1, -1);
EXPECT_STREQ("hello", div->innerText().utf8().data());
EXPECT_EQ(3u, controller().getSelectionOffsets().start());
EXPECT_EQ(3u, controller().getSelectionOffsets().end());
}
TEST_F(InputMethodControllerTest, CompositionInputEventIsComposing)
{
document().settings()->setScriptEnabled(true);
Element* editable = insertHTMLElement("<div id='sample' contentEditable='true'></div>", "sample");
Element* script = document().createElement("script", ASSERT_NO_EXCEPTION);
script->setInnerHTML(
"document.getElementById('sample').addEventListener('beforeinput', function(event) {"
" document.title = `beforeinput.isComposing:${event.isComposing};`;"
"});"
"document.getElementById('sample').addEventListener('input', function(event) {"
" document.title += `input.isComposing:${event.isComposing};`;"
"});",
ASSERT_NO_EXCEPTION);
document().body()->appendChild(script, ASSERT_NO_EXCEPTION);
document().view()->updateAllLifecyclePhases();
// Simulate composition in the |contentEditable|.
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
editable->focus();
document().setTitle(emptyString());
controller().setComposition("foo", underlines, 0, 3);
EXPECT_STREQ("beforeinput.isComposing:true;input.isComposing:true;", document().title().utf8().data());
document().setTitle(emptyString());
controller().confirmComposition();
// Last pair of InputEvent should also be inside composition scope.
EXPECT_STREQ("beforeinput.isComposing:true;input.isComposing:true;", document().title().utf8().data());
}
TEST_F(InputMethodControllerTest, CompositionInputEventData)
{
document().settings()->setScriptEnabled(true);
Element* editable = insertHTMLElement("<div id='sample' contentEditable='true'></div>", "sample");
Element* script = document().createElement("script", ASSERT_NO_EXCEPTION);
script->setInnerHTML(
"document.getElementById('sample').addEventListener('beforeinput', function(event) {"
" document.title = `beforeinput.data:${event.data};`;"
"});"
"document.getElementById('sample').addEventListener('input', function(event) {"
" document.title += `input.data:${event.data};`;"
"});",
ASSERT_NO_EXCEPTION);
document().body()->appendChild(script, ASSERT_NO_EXCEPTION);
document().view()->updateAllLifecyclePhases();
// Simulate composition in the |contentEditable|.
Vector<CompositionUnderline> underlines;
underlines.append(CompositionUnderline(0, 5, Color(255, 0, 0), false, 0));
editable->focus();
document().setTitle(emptyString());
controller().setComposition("n", underlines, 0, 1);
EXPECT_STREQ("beforeinput.data:n;input.data:n;", document().title().utf8().data());
document().setTitle(emptyString());
controller().setComposition("ni", underlines, 0, 1);
EXPECT_STREQ("beforeinput.data:ni;input.data:ni;", document().title().utf8().data());
document().setTitle(emptyString());
controller().confirmComposition();
EXPECT_STREQ("beforeinput.data:ni;input.data:ni;", document().title().utf8().data());
}
} // namespace blink