// Copyright 2017 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 #include "third_party/blink/renderer/core/editing/text_offset_mapping.h" #include "third_party/blink/renderer/core/editing/position.h" #include "third_party/blink/renderer/core/editing/selection_template.h" #include "third_party/blink/renderer/core/editing/testing/editing_test_base.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.h" #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" namespace blink { class ParameterizedTextOffsetMappingTest : public ::testing::WithParamInterface, private ScopedLayoutNGForTest, public EditingTestBase { protected: ParameterizedTextOffsetMappingTest() : ScopedLayoutNGForTest(GetParam()) {} std::string ComputeTextOffset(const std::string& selection_text) { const PositionInFlatTree position = ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base()); TextOffsetMapping mapping(GetInlineContents(position)); const String text = mapping.GetText(); const int offset = mapping.ComputeTextOffset(position); StringBuilder builder; builder.Append(text.Left(offset)); builder.Append('|'); builder.Append(text.Substring(offset)); return builder.ToString().Utf8(); } std::string GetRange(const std::string& selection_text) { const PositionInFlatTree position = ToPositionInFlatTree(SetSelectionTextToBody(selection_text).Base()); return GetRange(GetInlineContents(position)); } std::string GetRange(const TextOffsetMapping::InlineContents& contents) { TextOffsetMapping mapping(contents); return GetSelectionTextInFlatTreeFromBody( SelectionInFlatTree::Builder() .SetBaseAndExtent(mapping.GetRange()) .Build()); } std::string GetPositionBefore(const std::string& html_text, int offset) { SetBodyContent(html_text); TextOffsetMapping mapping(GetInlineContents( PositionInFlatTree(*GetDocument().body()->firstChild(), 0))); return GetSelectionTextInFlatTreeFromBody( SelectionInFlatTree::Builder() .Collapse(mapping.GetPositionBefore(offset)) .Build()); } std::string GetPositionAfter(const std::string& html_text, int offset) { SetBodyContent(html_text); TextOffsetMapping mapping(GetInlineContents( PositionInFlatTree(*GetDocument().body()->firstChild(), 0))); return GetSelectionTextInFlatTreeFromBody( SelectionInFlatTree::Builder() .Collapse(mapping.GetPositionAfter(offset)) .Build()); } private: static TextOffsetMapping::InlineContents GetInlineContents( const PositionInFlatTree& position) { const TextOffsetMapping::InlineContents inline_contents = TextOffsetMapping::FindForwardInlineContents(position); DCHECK(inline_contents.IsNotNull()) << position; return inline_contents; } }; INSTANTIATE_TEST_SUITE_P(All, ParameterizedTextOffsetMappingTest, ::testing::Bool()); TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetBasic) { EXPECT_EQ("|(1) abc def", ComputeTextOffset("

| (1) abc def

")); EXPECT_EQ("|(1) abc def", ComputeTextOffset("

|(1) abc def

")); EXPECT_EQ("(|1) abc def", ComputeTextOffset("

(|1) abc def

")); EXPECT_EQ("(1|) abc def", ComputeTextOffset("

(1|) abc def

")); EXPECT_EQ("(1)| abc def", ComputeTextOffset("

(1)| abc def

")); EXPECT_EQ("(1) |abc def", ComputeTextOffset("

(1) |abc def

")); EXPECT_EQ("(1) a|bc def", ComputeTextOffset("

(1) a|bc def

")); EXPECT_EQ("(1) ab|c def", ComputeTextOffset("

(1) ab|c def

")); EXPECT_EQ("(1) abc| def", ComputeTextOffset("

(1) abc| def

")); EXPECT_EQ("(1) abc |def", ComputeTextOffset("

(1) abc |def

")); EXPECT_EQ("(1) abc d|ef", ComputeTextOffset("

(1) abc d|ef

")); EXPECT_EQ("(1) abc de|f", ComputeTextOffset("

(1) abc de|f

")); EXPECT_EQ("(1) abc def|", ComputeTextOffset("

(1) abc def|

")); } TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithFirstLetter) { InsertStyleElement("p::first-letter {font-size:200%;}"); // Expectation should be as same as |ComputeTextOffsetBasic| EXPECT_EQ("|(1) abc def", ComputeTextOffset("

| (1) abc def

")); EXPECT_EQ("|(1) abc def", ComputeTextOffset("

|(1) abc def

")); EXPECT_EQ("(|1) abc def", ComputeTextOffset("

(|1) abc def

")); EXPECT_EQ("(1|) abc def", ComputeTextOffset("

(1|) abc def

")); EXPECT_EQ("(1)| abc def", ComputeTextOffset("

(1)| abc def

")); EXPECT_EQ("(1) |abc def", ComputeTextOffset("

(1) |abc def

")); EXPECT_EQ("(1) a|bc def", ComputeTextOffset("

(1) a|bc def

")); EXPECT_EQ("(1) ab|c def", ComputeTextOffset("

(1) ab|c def

")); EXPECT_EQ("(1) abc| def", ComputeTextOffset("

(1) abc| def

")); EXPECT_EQ("(1) abc |def", ComputeTextOffset("

(1) abc |def

")); EXPECT_EQ("(1) abc d|ef", ComputeTextOffset("

(1) abc d|ef

")); EXPECT_EQ("(1) abc de|f", ComputeTextOffset("

(1) abc de|f

")); EXPECT_EQ("(1) abc def|", ComputeTextOffset("

(1) abc def|

")); } TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithFloat) { InsertStyleElement("b { float:right; }"); EXPECT_EQ("|aBCDe", ComputeTextOffset("

|aBCDe

")); EXPECT_EQ("a|BCDe", ComputeTextOffset("

a|BCDe

")); EXPECT_EQ("a|BCDe", ComputeTextOffset("

a|BCDe

")); EXPECT_EQ("aB|CDe", ComputeTextOffset("

aB|CDe

")); EXPECT_EQ("aBC|De", ComputeTextOffset("

aBC|De

")); EXPECT_EQ("aBCD|e", ComputeTextOffset("

aBCD|e

")); EXPECT_EQ("aBCD|e", ComputeTextOffset("

aBCD|e

")); EXPECT_EQ("aBCDe|", ComputeTextOffset("

aBCDe|

")); } TEST_P(ParameterizedTextOffsetMappingTest, ComputeTextOffsetWithInlineBlock) { InsertStyleElement("b { display:inline-block; }"); EXPECT_EQ("|aBCDe", ComputeTextOffset("

|aBCDe

")); EXPECT_EQ("a|BCDe", ComputeTextOffset("

a|BCDe

")); EXPECT_EQ("a|BCDe", ComputeTextOffset("

a|BCDe

")); EXPECT_EQ("aB|CDe", ComputeTextOffset("

aB|CDe

")); EXPECT_EQ("aBC|De", ComputeTextOffset("

aBC|De

")); EXPECT_EQ("aBCD|e", ComputeTextOffset("

aBCD|e

")); EXPECT_EQ("aBCD|e", ComputeTextOffset("

aBCD|e

")); EXPECT_EQ("aBCDe|", ComputeTextOffset("

aBCDe|

")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfAnonymousBlock) { EXPECT_EQ("

abc

^def|

ghi

", GetRange("

abc

d|ef

ghi

")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockOnInlineBlock) { // display:inline-block doesn't introduce block. EXPECT_EQ("^abc

def
ghi

xyz|", GetRange("|abc

def
ghi

xyz")); EXPECT_EQ("^abc

def
ghi

xyz|", GetRange("abc

|def
ghi

xyz")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithAnonymousBlock) { // "abc" and "xyz" are in anonymous block. // Range is "abc" EXPECT_EQ("^abc|

def

xyz", GetRange("|abc

def

xyz")); EXPECT_EQ("^abc|

def

xyz", GetRange("a|bc

def

xyz")); // Range is "def" EXPECT_EQ("abc

^def|

xyz", GetRange("abc

|def

xyz")); EXPECT_EQ("abc

^def|

xyz", GetRange("abc

d|ef

xyz")); // Range is "xyz" EXPECT_EQ("abc

def

^xyz|", GetRange("abc

def

|xyz")); EXPECT_EQ("abc

def

^xyz|", GetRange("abc

def

xyz|")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithBR) { EXPECT_EQ("^abc
xyz|", GetRange("abc|
xyz")) << "BR doesn't affect block"; } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithPRE) { // "\n" doesn't affect block. EXPECT_EQ("
^abc\ndef\nghi\n|
", GetRange("
|abc\ndef\nghi\n
")); EXPECT_EQ("
^abc\ndef\nghi\n|
", GetRange("
abc\n|def\nghi\n
")); EXPECT_EQ("
^abc\ndef\nghi\n|
", GetRange("
abc\ndef\n|ghi\n
")); EXPECT_EQ("
^abc\ndef\nghi\n|
", GetRange("
abc\ndef\nghi\n|
")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithRUBY) { EXPECT_EQ("^abc|123", GetRange("|abc123")); EXPECT_EQ("abc^123|", GetRange("abc1|23")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithRUBYandBR) { EXPECT_EQ("^abc
def|123
456
", GetRange("|abc
def123
456
")) << "RT(LayoutRubyRun) is a block"; EXPECT_EQ("abc
def^123
456|
", GetRange("abc
def123|
456
")) << "RUBY introduce LayoutRuleBase for 'abc'"; } TEST_P(ParameterizedTextOffsetMappingTest, RangeOfBlockWithTABLE) { EXPECT_EQ("^abc|
one
xyz", GetRange("|abc
one
xyz")) << "Before TABLE"; EXPECT_EQ("abc
^one|
xyz", GetRange("abc
o|ne
xyz")) << "In TD"; EXPECT_EQ("abc
one
^xyz|", GetRange("abc
one
x|yz")) << "After TABLE"; } // |InlineContents| can represent an empty block. // See LinkSelectionClickEventsTest.SingleAndDoubleClickWillBeHandled TEST_P(ParameterizedTextOffsetMappingTest, RangeOfEmptyBlock) { const PositionInFlatTree position = ToPositionInFlatTree( SetSelectionTextToBody( "

abc

|

ghi

") .Base()); const LayoutObject* const target_layout_object = GetDocument().getElementById("target")->GetLayoutObject(); const TextOffsetMapping::InlineContents inline_contents = TextOffsetMapping::FindForwardInlineContents(position); ASSERT_TRUE(inline_contents.IsNotNull()); EXPECT_EQ(target_layout_object, inline_contents.GetEmptyBlock()); EXPECT_EQ(inline_contents, TextOffsetMapping::FindBackwardInlineContents(position)); } // http://crbug.com/900906 TEST_P(ParameterizedTextOffsetMappingTest, AnonymousBlockFlowWrapperForFloatPseudo) { InsertStyleElement("table::after{content:close-quote;float:right;}"); const PositionInFlatTree position = ToPositionInFlatTree(SetCaretTextToBody("
|foo")); const TextOffsetMapping::InlineContents inline_contents = TextOffsetMapping::FindBackwardInlineContents(position); ASSERT_TRUE(inline_contents.IsNotNull()); const TextOffsetMapping::InlineContents previous_contents = TextOffsetMapping::InlineContents::PreviousOf(inline_contents); EXPECT_TRUE(previous_contents.IsNull()); } TEST_P(ParameterizedTextOffsetMappingTest, ForwardRangesWithTextControl) { // InlineContents for positions outside text control should cover the entire // containing block. const PositionInFlatTree outside_position = ToPositionInFlatTree( SetCaretTextToBody("foobar")); const TextOffsetMapping::InlineContents outside_contents = TextOffsetMapping::FindForwardInlineContents(outside_position); EXPECT_EQ("^foo
bla
bar|", GetRange(outside_contents)); // InlineContents for positions inside text control should not escape the text // control in forward iteration. const Element* input = GetDocument().QuerySelector("input"); const PositionInFlatTree inside_first = PositionInFlatTree::FirstPositionInNode(*input); const TextOffsetMapping::InlineContents inside_contents = TextOffsetMapping::FindForwardInlineContents(inside_first); EXPECT_EQ("foo
^bla|
bar", GetRange(inside_contents)); EXPECT_TRUE( TextOffsetMapping::InlineContents::NextOf(inside_contents).IsNull()); const PositionInFlatTree inside_last = PositionInFlatTree::LastPositionInNode(*input); EXPECT_TRUE( TextOffsetMapping::FindForwardInlineContents(inside_last).IsNull()); } TEST_P(ParameterizedTextOffsetMappingTest, BackwardRangesWithTextControl) { // InlineContents for positions outside text control should cover the entire // containing block. const PositionInFlatTree outside_position = ToPositionInFlatTree( SetCaretTextToBody("foobar")); const TextOffsetMapping::InlineContents outside_contents = TextOffsetMapping::FindBackwardInlineContents(outside_position); EXPECT_EQ("^foo
bla
bar|", GetRange(outside_contents)); // InlineContents for positions inside text control should not escape the text // control in backward iteration. const Element* input = GetDocument().QuerySelector("input"); const PositionInFlatTree inside_last = PositionInFlatTree::LastPositionInNode(*input); const TextOffsetMapping::InlineContents inside_contents = TextOffsetMapping::FindBackwardInlineContents(inside_last); EXPECT_EQ("foo
^bla|
bar", GetRange(inside_contents)); EXPECT_TRUE( TextOffsetMapping::InlineContents::PreviousOf(inside_contents).IsNull()); const PositionInFlatTree inside_first = PositionInFlatTree::FirstPositionInNode(*input); EXPECT_TRUE( TextOffsetMapping::FindBackwardInlineContents(inside_first).IsNull()); } // http://crbug.com/832497 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithCollapsedWhitespace) { // Whitespaces after
is collapsed. EXPECT_EQ("
^|
", GetRange("|
")); } // http://crbug.com//832055 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithMulticol) { InsertStyleElement("div { columns: 3 100px; }"); EXPECT_EQ("
^foo|
", GetRange("
foo|
")); } // http://crbug.com/832101 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithNestedFloat) { InsertStyleElement("b, i { float: right; }"); // Note: Legacy: BODY is inline, NG: BODY is block. EXPECT_EQ("^abc def ghixyz|", GetRange("abc d|ef ghixyz")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeWithNestedInlineBlock) { InsertStyleElement("b, i { display: inline-block; }"); EXPECT_EQ("^a b de|", GetRange("|a b de")); EXPECT_EQ("^a b de|", GetRange("|a b de")); EXPECT_EQ("^a b de|", GetRange("a| b de")); EXPECT_EQ("^a b de|", GetRange("a |b de")); EXPECT_EQ("^a b de|", GetRange("a |b de")); EXPECT_EQ("^a b de|", GetRange("a b| de")); EXPECT_EQ("^a b de|", GetRange("a b| de")); EXPECT_EQ("^a b de|", GetRange("a b |de")); EXPECT_EQ("^a b de|", GetRange("a b d|e")); EXPECT_EQ("^a b de|", GetRange("a b d|e")); EXPECT_EQ("^a b de|", GetRange("a b de|")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeWithInlineBlockBlock) { InsertStyleElement("b { display:inline-block; }"); // TODO(editing-dev): We should have "^ab|

" EXPECT_EQ("^ab

c

de|", GetRange("|ab

c

d
e")); EXPECT_EQ("^ab

c

d
e|", GetRange("a|b

c

d
e")); EXPECT_EQ("a^b|

c

d
e", GetRange("a|b

c

d
e")); EXPECT_EQ("a^b|

c

d
e", GetRange("ab|

c

d
e")); EXPECT_EQ("ab

^c|

d
e", GetRange("ab

|c

d
e")); EXPECT_EQ("ab

^c|

d
e", GetRange("ab

c|

d
e")); EXPECT_EQ("ab

c

^d|
e", GetRange("ab

c

|d
e")); EXPECT_EQ("^ab

c

d
e|", GetRange("ab

c

d
|e")); EXPECT_EQ("^ab

c

d
e|", GetRange("ab

c

d
e|")); } TEST_P(ParameterizedTextOffsetMappingTest, RangeWithInlineBlockBlocks) { InsertStyleElement("b { display:inline-block; }"); // TODO(editing-dev): We should have "^a|" EXPECT_EQ("^a

b

c

d|", GetRange("|a

b

c

d")); EXPECT_EQ("^a

b

c

d|", GetRange("a|

b

c

d")); EXPECT_EQ("a

^b|

c

d", GetRange("a|

b

c

d")); EXPECT_EQ("a

^b|

c

d", GetRange("a

|b

c

d")); EXPECT_EQ("a

^b|

c

d", GetRange("a

b|

c

d")); EXPECT_EQ("a

b

^c|

d", GetRange("a

b

|

c

d")); EXPECT_EQ("a

b

^c|

d", GetRange("a

b

|c

d")); EXPECT_EQ("a

b

^c|

d", GetRange("a

b

c|

d")); EXPECT_EQ("^a

b

c

d|", GetRange("a

b

c

|
d")); EXPECT_EQ("^a

b

c

d|", GetRange("a

b

c

|d")); EXPECT_EQ("^a

b

c

d|", GetRange("a

b

c

d|")); } // http://crbug.com/832101 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithNestedPosition) { InsertStyleElement("b, i { position: fixed; }"); EXPECT_EQ("abc ^def| ghixyz", GetRange("abc d|ef ghixyz")); } // http://crbug.com//834623 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithSelect) { EXPECT_EQ( "^foo|", GetRange("foo")); } // http://crbug.com//832350 TEST_P(ParameterizedTextOffsetMappingTest, RangeWithShadowDOM) { EXPECT_EQ("
^abc|
", GetRange("
" "" "|abc" "
")); } TEST_P(ParameterizedTextOffsetMappingTest, GetPositionBefore) { EXPECT_EQ(" |012 456 ", GetPositionBefore(" 012 456 ", 0)); EXPECT_EQ(" 0|12 456 ", GetPositionBefore(" 012 456 ", 1)); EXPECT_EQ(" 01|2 456 ", GetPositionBefore(" 012 456 ", 2)); EXPECT_EQ(" 012| 456 ", GetPositionBefore(" 012 456 ", 3)); EXPECT_EQ(" 012 |456 ", GetPositionBefore(" 012 456 ", 4)); EXPECT_EQ(" 012 4|56 ", GetPositionBefore(" 012 456 ", 5)); EXPECT_EQ(" 012 45|6 ", GetPositionBefore(" 012 456 ", 6)); EXPECT_EQ(" 012 456| ", GetPositionBefore(" 012 456 ", 7)); // We hit DCHECK for offset 8, because we walk on "012 456". } TEST_P(ParameterizedTextOffsetMappingTest, GetPositionAfter) { EXPECT_EQ(" 0|12 456 ", GetPositionAfter(" 012 456 ", 0)); EXPECT_EQ(" 01|2 456 ", GetPositionAfter(" 012 456 ", 1)); EXPECT_EQ(" 012| 456 ", GetPositionAfter(" 012 456 ", 2)); EXPECT_EQ(" 012 | 456 ", GetPositionAfter(" 012 456 ", 3)); EXPECT_EQ(" 012 4|56 ", GetPositionAfter(" 012 456 ", 4)); EXPECT_EQ(" 012 45|6 ", GetPositionAfter(" 012 456 ", 5)); EXPECT_EQ(" 012 456| ", GetPositionAfter(" 012 456 ", 6)); EXPECT_EQ(" 012 456 |", GetPositionAfter(" 012 456 ", 7)); // We hit DCHECK for offset 8, because we walk on "012 456". } // https://crbug.com/903723 TEST_P(ParameterizedTextOffsetMappingTest, InlineContentsWithDocumentBoundary) { InsertStyleElement("*{position:fixed}"); SetBodyContent(""); const PositionInFlatTree position = PositionInFlatTree::FirstPositionInNode(*GetDocument().body()); const TextOffsetMapping::InlineContents inline_contents = TextOffsetMapping::FindForwardInlineContents(position); EXPECT_TRUE(inline_contents.IsNotNull()); // Should not crash when previous/next iteration reaches document boundary. EXPECT_TRUE( TextOffsetMapping::InlineContents::PreviousOf(inline_contents).IsNull()); EXPECT_TRUE( TextOffsetMapping::InlineContents::NextOf(inline_contents).IsNull()); } } // namespace blink