| // 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 "third_party/blink/renderer/core/dom/range.h" |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/bindings/core/v8/string_or_array_buffer_or_array_buffer_view.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/core/css/font_face_descriptors.h" |
| #include "third_party/blink/renderer/core/css/font_face_set_document.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node_list.h" |
| #include "third_party/blink/renderer/core/dom/text.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.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/editing/visible_units.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/html/forms/html_text_area_element.h" |
| #include "third_party/blink/renderer/core/html/html_body_element.h" |
| #include "third_party/blink/renderer/core/html/html_div_element.h" |
| #include "third_party/blink/renderer/core/html/html_document.h" |
| #include "third_party/blink/renderer/core/html/html_element.h" |
| #include "third_party/blink/renderer/core/html/html_html_element.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/geometry/float_quad.h" |
| #include "third_party/blink/renderer/platform/heap/handle.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/wtf/compiler.h" |
| #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" |
| |
| namespace blink { |
| |
| class RangeTest : public EditingTestBase {}; |
| |
| TEST_F(RangeTest, extractContentsWithDOMMutationEvent) { |
| GetDocument().body()->SetInnerHTMLFromString("<span><b>abc</b>def</span>"); |
| GetDocument().GetSettings()->SetScriptEnabled(true); |
| Element* const script_element = |
| GetDocument().CreateRawElement(html_names::kScriptTag); |
| script_element->setTextContent( |
| "let count = 0;" |
| "const span = document.querySelector('span');" |
| "span.addEventListener('DOMSubtreeModified', () => {" |
| " if (++count > 1) return;" |
| " span.firstChild.textContent = 'ABC';" |
| " span.lastChild.textContent = 'DEF';" |
| "});"); |
| GetDocument().body()->AppendChild(script_element); |
| |
| Element* const span_element = GetDocument().QuerySelector("span"); |
| Range* const range = |
| Range::Create(GetDocument(), span_element, 0, span_element, 1); |
| Element* const result = GetDocument().CreateRawElement(html_names::kDivTag); |
| result->AppendChild(range->extractContents(ASSERT_NO_EXCEPTION)); |
| |
| EXPECT_EQ("<b>abc</b>", result->InnerHTMLAsString()) |
| << "DOM mutation event handler should not affect result."; |
| EXPECT_EQ("<span>DEF</span>", span_element->OuterHTMLAsString()) |
| << "DOM mutation event handler should be executed."; |
| } |
| |
| // http://crbug.com/822510 |
| TEST_F(RangeTest, IntersectsNode) { |
| SetBodyContent( |
| "<div>" |
| "<span id='s0'>s0</span>" |
| "<span id='s1'>s1</span>" |
| "<span id='s2'>s2</span>" |
| "</div>"); |
| Element* const div = GetDocument().QuerySelector("div"); |
| Element* const s0 = GetDocument().getElementById("s0"); |
| Element* const s1 = GetDocument().getElementById("s1"); |
| Element* const s2 = GetDocument().getElementById("s2"); |
| Range& range = *Range::Create(GetDocument()); |
| |
| // Range encloses s0 |
| range.setStart(div, 0); |
| range.setEnd(div, 1); |
| EXPECT_TRUE(range.intersectsNode(s0, ASSERT_NO_EXCEPTION)); |
| EXPECT_FALSE(range.intersectsNode(s1, ASSERT_NO_EXCEPTION)); |
| EXPECT_FALSE(range.intersectsNode(s2, ASSERT_NO_EXCEPTION)); |
| |
| // Range encloses s1 |
| range.setStart(div, 1); |
| range.setEnd(div, 2); |
| EXPECT_FALSE(range.intersectsNode(s0, ASSERT_NO_EXCEPTION)); |
| EXPECT_TRUE(range.intersectsNode(s1, ASSERT_NO_EXCEPTION)); |
| EXPECT_FALSE(range.intersectsNode(s2, ASSERT_NO_EXCEPTION)); |
| |
| // Range encloses s2 |
| range.setStart(div, 2); |
| range.setEnd(div, 3); |
| EXPECT_FALSE(range.intersectsNode(s0, ASSERT_NO_EXCEPTION)); |
| EXPECT_FALSE(range.intersectsNode(s1, ASSERT_NO_EXCEPTION)); |
| EXPECT_TRUE(range.intersectsNode(s2, ASSERT_NO_EXCEPTION)); |
| } |
| |
| TEST_F(RangeTest, SplitTextNodeRangeWithinText) { |
| V8TestingScope scope; |
| |
| GetDocument().body()->SetInnerHTMLFromString("1234"); |
| Text* old_text = ToText(GetDocument().body()->firstChild()); |
| |
| Range* range04 = Range::Create(GetDocument(), old_text, 0, old_text, 4); |
| Range* range02 = Range::Create(GetDocument(), old_text, 0, old_text, 2); |
| Range* range22 = Range::Create(GetDocument(), old_text, 2, old_text, 2); |
| Range* range24 = Range::Create(GetDocument(), old_text, 2, old_text, 4); |
| |
| old_text->splitText(2, ASSERT_NO_EXCEPTION); |
| Text* new_text = ToText(old_text->nextSibling()); |
| |
| EXPECT_TRUE(range04->BoundaryPointsValid()); |
| EXPECT_EQ(old_text, range04->startContainer()); |
| EXPECT_EQ(0u, range04->startOffset()); |
| EXPECT_EQ(new_text, range04->endContainer()); |
| EXPECT_EQ(2u, range04->endOffset()); |
| |
| EXPECT_TRUE(range02->BoundaryPointsValid()); |
| EXPECT_EQ(old_text, range02->startContainer()); |
| EXPECT_EQ(0u, range02->startOffset()); |
| EXPECT_EQ(old_text, range02->endContainer()); |
| EXPECT_EQ(2u, range02->endOffset()); |
| |
| // Our implementation always moves the boundary point at the separation point |
| // to the end of the original text node. |
| EXPECT_TRUE(range22->BoundaryPointsValid()); |
| EXPECT_EQ(old_text, range22->startContainer()); |
| EXPECT_EQ(2u, range22->startOffset()); |
| EXPECT_EQ(old_text, range22->endContainer()); |
| EXPECT_EQ(2u, range22->endOffset()); |
| |
| EXPECT_TRUE(range24->BoundaryPointsValid()); |
| EXPECT_EQ(old_text, range24->startContainer()); |
| EXPECT_EQ(2u, range24->startOffset()); |
| EXPECT_EQ(new_text, range24->endContainer()); |
| EXPECT_EQ(2u, range24->endOffset()); |
| } |
| |
| TEST_F(RangeTest, SplitTextNodeRangeOutsideText) { |
| V8TestingScope scope; |
| |
| GetDocument().body()->SetInnerHTMLFromString( |
| "<span id=\"outer\">0<span id=\"inner-left\">1</span>SPLITME<span " |
| "id=\"inner-right\">2</span>3</span>"); |
| |
| Element* outer = |
| GetDocument().getElementById(AtomicString::FromUTF8("outer")); |
| Element* inner_left = |
| GetDocument().getElementById(AtomicString::FromUTF8("inner-left")); |
| Element* inner_right = |
| GetDocument().getElementById(AtomicString::FromUTF8("inner-right")); |
| Text* old_text = ToText(outer->childNodes()->item(2)); |
| |
| Range* range_outer_outside = Range::Create(GetDocument(), outer, 0, outer, 5); |
| Range* range_outer_inside = Range::Create(GetDocument(), outer, 1, outer, 4); |
| Range* range_outer_surrounding_text = |
| Range::Create(GetDocument(), outer, 2, outer, 3); |
| Range* range_inner_left = |
| Range::Create(GetDocument(), inner_left, 0, inner_left, 1); |
| Range* range_inner_right = |
| Range::Create(GetDocument(), inner_right, 0, inner_right, 1); |
| Range* range_from_text_to_middle_of_element = |
| Range::Create(GetDocument(), old_text, 6, outer, 3); |
| |
| old_text->splitText(3, ASSERT_NO_EXCEPTION); |
| Text* new_text = ToText(old_text->nextSibling()); |
| |
| EXPECT_TRUE(range_outer_outside->BoundaryPointsValid()); |
| EXPECT_EQ(outer, range_outer_outside->startContainer()); |
| EXPECT_EQ(0u, range_outer_outside->startOffset()); |
| EXPECT_EQ(outer, range_outer_outside->endContainer()); |
| EXPECT_EQ(6u, |
| range_outer_outside |
| ->endOffset()); // Increased by 1 since a new node is inserted. |
| |
| EXPECT_TRUE(range_outer_inside->BoundaryPointsValid()); |
| EXPECT_EQ(outer, range_outer_inside->startContainer()); |
| EXPECT_EQ(1u, range_outer_inside->startOffset()); |
| EXPECT_EQ(outer, range_outer_inside->endContainer()); |
| EXPECT_EQ(5u, range_outer_inside->endOffset()); |
| |
| EXPECT_TRUE(range_outer_surrounding_text->BoundaryPointsValid()); |
| EXPECT_EQ(outer, range_outer_surrounding_text->startContainer()); |
| EXPECT_EQ(2u, range_outer_surrounding_text->startOffset()); |
| EXPECT_EQ(outer, range_outer_surrounding_text->endContainer()); |
| EXPECT_EQ(4u, range_outer_surrounding_text->endOffset()); |
| |
| EXPECT_TRUE(range_inner_left->BoundaryPointsValid()); |
| EXPECT_EQ(inner_left, range_inner_left->startContainer()); |
| EXPECT_EQ(0u, range_inner_left->startOffset()); |
| EXPECT_EQ(inner_left, range_inner_left->endContainer()); |
| EXPECT_EQ(1u, range_inner_left->endOffset()); |
| |
| EXPECT_TRUE(range_inner_right->BoundaryPointsValid()); |
| EXPECT_EQ(inner_right, range_inner_right->startContainer()); |
| EXPECT_EQ(0u, range_inner_right->startOffset()); |
| EXPECT_EQ(inner_right, range_inner_right->endContainer()); |
| EXPECT_EQ(1u, range_inner_right->endOffset()); |
| |
| EXPECT_TRUE(range_from_text_to_middle_of_element->BoundaryPointsValid()); |
| EXPECT_EQ(new_text, range_from_text_to_middle_of_element->startContainer()); |
| EXPECT_EQ(3u, range_from_text_to_middle_of_element->startOffset()); |
| EXPECT_EQ(outer, range_from_text_to_middle_of_element->endContainer()); |
| EXPECT_EQ(4u, range_from_text_to_middle_of_element->endOffset()); |
| } |
| |
| TEST_F(RangeTest, updateOwnerDocumentIfNeeded) { |
| Element* foo = GetDocument().CreateElementForBinding("foo"); |
| Element* bar = GetDocument().CreateElementForBinding("bar"); |
| foo->AppendChild(bar); |
| |
| Range* range = |
| Range::Create(GetDocument(), Position(bar, 0), Position(foo, 1)); |
| |
| Document* another_document = Document::CreateForTest(); |
| another_document->AppendChild(foo); |
| |
| EXPECT_EQ(bar, range->startContainer()); |
| EXPECT_EQ(0u, range->startOffset()); |
| EXPECT_EQ(foo, range->endContainer()); |
| EXPECT_EQ(1u, range->endOffset()); |
| } |
| |
| // Regression test for crbug.com/639184 |
| TEST_F(RangeTest, NotMarkedValidByIrrelevantTextInsert) { |
| GetDocument().body()->SetInnerHTMLFromString( |
| "<div><span id=span1>foo</span>bar<span id=span2>baz</span></div>"); |
| |
| Element* div = GetDocument().QuerySelector("div"); |
| Element* span1 = GetDocument().getElementById("span1"); |
| Element* span2 = GetDocument().getElementById("span2"); |
| Text* text = ToText(div->childNodes()->item(1)); |
| |
| Range* range = Range::Create(GetDocument(), span2, 0, div, 3); |
| |
| div->RemoveChild(span1); |
| text->insertData(0, "bar", ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(range->BoundaryPointsValid()); |
| EXPECT_EQ(span2, range->startContainer()); |
| EXPECT_EQ(0u, range->startOffset()); |
| EXPECT_EQ(div, range->endContainer()); |
| EXPECT_EQ(2u, range->endOffset()); |
| } |
| |
| // Regression test for crbug.com/639184 |
| TEST_F(RangeTest, NotMarkedValidByIrrelevantTextRemove) { |
| GetDocument().body()->SetInnerHTMLFromString( |
| "<div><span id=span1>foofoo</span>bar<span id=span2>baz</span></div>"); |
| |
| Element* div = GetDocument().QuerySelector("div"); |
| Element* span1 = GetDocument().getElementById("span1"); |
| Element* span2 = GetDocument().getElementById("span2"); |
| Text* text = ToText(div->childNodes()->item(1)); |
| |
| Range* range = Range::Create(GetDocument(), span2, 0, div, 3); |
| |
| div->RemoveChild(span1); |
| text->deleteData(0, 3, ASSERT_NO_EXCEPTION); |
| |
| EXPECT_TRUE(range->BoundaryPointsValid()); |
| EXPECT_EQ(span2, range->startContainer()); |
| EXPECT_EQ(0u, range->startOffset()); |
| EXPECT_EQ(div, range->endContainer()); |
| EXPECT_EQ(2u, range->endOffset()); |
| } |
| |
| // Regression test for crbug.com/698123 |
| TEST_F(RangeTest, ExpandNotCrash) { |
| Range* range = Range::Create(GetDocument()); |
| Node* div = HTMLDivElement::Create(GetDocument()); |
| range->setStart(div, 0, ASSERT_NO_EXCEPTION); |
| range->expand("", ASSERT_NO_EXCEPTION); |
| } |
| |
| TEST_F(RangeTest, ToPosition) { |
| Node& textarea = *HTMLTextAreaElement::Create(GetDocument()); |
| Range& range = *Range::Create(GetDocument()); |
| const Position position = Position(&textarea, 0); |
| range.setStart(position, ASSERT_NO_EXCEPTION); |
| EXPECT_EQ(position, range.StartPosition()); |
| EXPECT_EQ(position, range.EndPosition()); |
| } |
| |
| TEST_F(RangeTest, BoundingRectMustIndependentFromSelection) { |
| LoadAhem(); |
| GetDocument().body()->SetInnerHTMLFromString( |
| "<div style='font: Ahem; width: 2em;letter-spacing: 5px;'>xx xx </div>"); |
| Node* const div = GetDocument().QuerySelector("div"); |
| // "x^x |
| // x|x " |
| Range* const range = |
| Range::Create(GetDocument(), div->firstChild(), 1, div->firstChild(), 4); |
| const FloatRect rect_before = range->BoundingRect(); |
| EXPECT_GT(rect_before.Width(), 0); |
| EXPECT_GT(rect_before.Height(), 0); |
| Selection().SetSelectionAndEndTyping( |
| SelectionInDOMTree::Builder() |
| .SetBaseAndExtent(EphemeralRange(range)) |
| .Build()); |
| UpdateAllLifecyclePhasesForTest(); |
| EXPECT_EQ(Selection().SelectedText(), "x x"); |
| const FloatRect rect_after = range->BoundingRect(); |
| EXPECT_EQ(rect_before, rect_after); |
| } |
| |
| // Regression test for crbug.com/681536 |
| TEST_F(RangeTest, BorderAndTextQuadsWithInputInBetween) { |
| GetDocument().body()->SetInnerHTMLFromString( |
| "<div>foo <u><input> bar</u></div>"); |
| GetDocument().UpdateStyleAndLayout(); |
| |
| Node* foo = GetDocument().QuerySelector("div")->firstChild(); |
| Node* bar = GetDocument().QuerySelector("u")->lastChild(); |
| Range* range = Range::Create(GetDocument(), foo, 2, bar, 2); |
| |
| Vector<FloatQuad> quads; |
| range->GetBorderAndTextQuads(quads); |
| |
| // Should get one quad for "o ", <input> and " b", respectively. |
| ASSERT_EQ(3u, quads.size()); |
| } |
| |
| static Vector<FloatQuad> GetBorderAndTextQuads(const Position& start, |
| const Position& end) { |
| DCHECK_LE(start, end); |
| Range* const range = Range::Create(*start.GetDocument(), start, end); |
| Vector<FloatQuad> quads; |
| range->GetBorderAndTextQuads(quads); |
| return quads; |
| } |
| |
| static Vector<IntSize> ComputeSizesOfQuads(const Vector<FloatQuad>& quads) { |
| Vector<IntSize> sizes; |
| for (const auto& quad : quads) |
| sizes.push_back(quad.EnclosingBoundingBox().Size()); |
| return sizes; |
| } |
| |
| TEST_F(RangeTest, GetBorderAndTextQuadsWithFirstLetterOne) { |
| GetDocument().body()->SetInnerHTMLFromString(R"HTML( |
| <style> |
| body { font-size: 20px; } |
| #sample::first-letter { font-size: 500%; } |
| </style> |
| <p id=sample>abc</p> |
| <p id=expected><span style='font-size: 500%'>a</span>bc</p> |
| )HTML"); |
| GetDocument().UpdateStyleAndLayout(); |
| |
| Element* const expected = GetDocument().getElementById("expected"); |
| Element* const sample = GetDocument().getElementById("sample"); |
| |
| const Vector<FloatQuad> expected_quads = |
| GetBorderAndTextQuads(Position(expected, 0), Position(expected, 2)); |
| const Vector<FloatQuad> sample_quads = |
| GetBorderAndTextQuads(Position(sample, 0), Position(sample, 1)); |
| ASSERT_EQ(2u, sample_quads.size()); |
| ASSERT_EQ(3u, expected_quads.size()) |
| << "expected_quads has SPAN, SPAN.firstChild and P.lastChild"; |
| EXPECT_EQ(expected_quads[0].EnclosingBoundingBox().Size(), |
| sample_quads[0].EnclosingBoundingBox().Size()) |
| << "Check size of first-letter part"; |
| EXPECT_EQ(expected_quads[2].EnclosingBoundingBox().Size(), |
| sample_quads[1].EnclosingBoundingBox().Size()) |
| << "Check size of first-letter part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(expected->firstChild(), 0), |
| Position(expected->firstChild(), 1))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 0), |
| Position(sample->firstChild(), 1)))) |
| << "All first-letter part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(expected->lastChild(), 0), |
| Position(expected->lastChild(), 2))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 1), |
| Position(sample->firstChild(), 3)))) |
| << "All remaining part"; |
| } |
| |
| TEST_F(RangeTest, GetBorderAndTextQuadsWithFirstLetterThree) { |
| GetDocument().body()->SetInnerHTMLFromString(R"HTML( |
| <style> |
| body { font-size: 20px; } |
| #sample::first-letter { font-size: 500%; } |
| </style> |
| <p id=sample>(a)bc</p> |
| <p id=expected><span style='font-size: 500%'>(a)</span>bc</p> |
| )HTML"); |
| GetDocument().UpdateStyleAndLayout(); |
| |
| Element* const expected = GetDocument().getElementById("expected"); |
| Element* const sample = GetDocument().getElementById("sample"); |
| |
| const Vector<FloatQuad> expected_quads = |
| GetBorderAndTextQuads(Position(expected, 0), Position(expected, 2)); |
| const Vector<FloatQuad> sample_quads = |
| GetBorderAndTextQuads(Position(sample, 0), Position(sample, 1)); |
| ASSERT_EQ(2u, sample_quads.size()); |
| ASSERT_EQ(3u, expected_quads.size()) |
| << "expected_quads has SPAN, SPAN.firstChild and P.lastChild"; |
| EXPECT_EQ(expected_quads[0].EnclosingBoundingBox().Size(), |
| sample_quads[0].EnclosingBoundingBox().Size()) |
| << "Check size of first-letter part"; |
| EXPECT_EQ(expected_quads[2].EnclosingBoundingBox().Size(), |
| sample_quads[1].EnclosingBoundingBox().Size()) |
| << "Check size of first-letter part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(expected->firstChild(), 0), |
| Position(expected->firstChild(), 1))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 0), |
| Position(sample->firstChild(), 3)))) |
| << "All first-letter part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(expected->lastChild(), 0), |
| Position(expected->lastChild(), 2))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 3), |
| Position(sample->firstChild(), 5)))) |
| << "All remaining part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads(GetBorderAndTextQuads( |
| Position(expected->firstChild()->firstChild(), 1), |
| Position(expected->firstChild()->firstChild(), 2))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 1), |
| Position(sample->firstChild(), 2)))) |
| << "Partial first-letter part"; |
| |
| EXPECT_EQ(ComputeSizesOfQuads(GetBorderAndTextQuads( |
| Position(expected->firstChild()->firstChild(), 1), |
| Position(expected->lastChild(), 1))), |
| ComputeSizesOfQuads( |
| GetBorderAndTextQuads(Position(sample->firstChild(), 1), |
| Position(sample->firstChild(), 4)))) |
| << "Partial first-letter part and remaining part"; |
| } |
| |
| } // namespace blink |