| // Copyright 2018 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/editing/selection_adjuster.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/html/forms/text_control_element.h" |
| |
| namespace blink { |
| |
| class SelectionAdjusterTest : public EditingTestBase {}; |
| |
| // ------------ Shadow boundary adjustment tests -------------- |
| TEST_F(SelectionAdjusterTest, AdjustShadowToCollpasedInDOMTree) { |
| const SelectionInDOMTree& selection = SetSelectionTextToBody( |
| "<span><template data-mode=\"open\">a|bc</template></span>^"); |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries( |
| selection); |
| EXPECT_EQ("<span></span>|", GetSelectionTextFromBody(result)); |
| } |
| |
| TEST_F(SelectionAdjusterTest, AdjustShadowToCollpasedInFlatTree) { |
| SetBodyContent("<input value=abc>"); |
| const auto& input = ToTextControl(*GetDocument().QuerySelector("input")); |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::AfterNode(input)) |
| .Extend( |
| PositionInFlatTree(*input.InnerEditorElement()->firstChild(), 1)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingShadowBoundaries( |
| selection); |
| EXPECT_EQ("<input value=\"abc\"><div>abc</div></input>|", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| } |
| |
| // ------------ Editing boundary adjustment tests -------------- |
| // Extracted the related part from delete-non-editable-range-crash.html here, |
| // because the final result in that test was not WAI. |
| TEST_F(SelectionAdjusterTest, DeleteNonEditableRange) { |
| const SelectionInDOMTree& selection = SetSelectionTextToBody(R"HTML( |
| <div contenteditable> |
| <blockquote> |
| <span>^foo<br></span> |
| barbarbar |
| </blockquote> |
| <span contenteditable="false"> |
| <span contenteditable>|</span> |
| <ol>bar</ol> |
| </span> |
| </div>)HTML"); |
| |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| |
| EXPECT_EQ(R"HTML( |
| <div contenteditable> |
| <blockquote> |
| <span>^foo<br></span> |
| barbarbar |
| </blockquote> |
| |<span contenteditable="false"> |
| <span contenteditable></span> |
| <ol>bar</ol> |
| </span> |
| </div>)HTML", |
| GetSelectionTextFromBody(result)); |
| } |
| |
| // Extracted the related part from format-block-contenteditable-false.html here, |
| // because the final result in that test was not WAI. |
| TEST_F(SelectionAdjusterTest, FormatBlockContentEditableFalse) { |
| const SelectionInDOMTree& selection = SetSelectionTextToBody(R"HTML( |
| <div contenteditable> |
| <h1><i>^foo</i><br><i>baz</i></h1> |
| <div contenteditable="false">|bar</div> |
| </div>)HTML"); |
| |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| |
| EXPECT_EQ(R"HTML( |
| <div contenteditable> |
| <h1><i>^foo</i><br><i>baz</i></h1> |
| |<div contenteditable="false">bar</div> |
| </div>)HTML", |
| GetSelectionTextFromBody(result)); |
| } |
| |
| TEST_F(SelectionAdjusterTest, NestedContentEditableElements) { |
| // Select from bar to foo. |
| const SelectionInDOMTree& selection = SetSelectionTextToBody(R"HTML( |
| <div contenteditable> |
| <div contenteditable="false"> |
| <div contenteditable> |
| |foo |
| </div> |
| </div> |
| <br> |
| bar^ |
| </div>)HTML"); |
| |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| |
| EXPECT_EQ(R"HTML( |
| <div contenteditable> |
| <div contenteditable="false"> |
| <div contenteditable> |
| foo |
| </div> |
| </div>| |
| <br> |
| bar^ |
| </div>)HTML", |
| GetSelectionTextFromBody(result)); |
| } |
| |
| TEST_F(SelectionAdjusterTest, ShadowRootAsRootBoundaryElement) { |
| const char* body_content = "<div id='host'></div>"; |
| const char* shadow_content = "<div id='foo'>foo</div><div id='bar'>bar</div>"; |
| SetBodyContent(body_content); |
| ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host"); |
| |
| Element* foo = shadow_root->QuerySelector("#foo"); |
| Element* bar = shadow_root->QuerySelector("#bar"); |
| |
| // DOM tree selection. |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .Collapse(Position::FirstPositionInNode(*foo)) |
| .Extend(Position::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| |
| EXPECT_EQ(Position::FirstPositionInNode(*foo), result.Base()); |
| EXPECT_EQ(Position::LastPositionInNode(*bar), result.Extent()); |
| |
| // Flat tree selection. |
| const SelectionInFlatTree& selection_in_flat_tree = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInFlatTree& result_in_flat_tree = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection_in_flat_tree); |
| |
| EXPECT_EQ(PositionInFlatTree::FirstPositionInNode(*foo), |
| result_in_flat_tree.Base()); |
| EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*bar), |
| result_in_flat_tree.Extent()); |
| } |
| |
| TEST_F(SelectionAdjusterTest, ShadowRootAsRootBoundaryElementEditable) { |
| const char* body_content = "<div id='host'></div>"; |
| const char* shadow_content = |
| "foo" |
| "<div id='bar' contenteditable>bar</div>"; |
| SetBodyContent(body_content); |
| ShadowRoot* shadow_root = SetShadowContent(shadow_content, "host"); |
| |
| const Node* foo = shadow_root->firstChild(); |
| const Element* bar = shadow_root->QuerySelector("#bar"); |
| |
| // Select from foo to bar in DOM tree. |
| const SelectionInDOMTree& selection = |
| SelectionInDOMTree::Builder() |
| .Collapse(Position::FirstPositionInNode(*foo)) |
| .Extend(Position::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInDOMTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| |
| EXPECT_EQ(Position::FirstPositionInNode(*foo), result.Base()); |
| EXPECT_EQ(Position::BeforeNode(*bar), result.Extent()); |
| |
| // Select from foo to bar in flat tree. |
| const SelectionInFlatTree& selection_in_flat_tree = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInFlatTree& result_in_flat_tree = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection_in_flat_tree); |
| |
| EXPECT_EQ(PositionInFlatTree::FirstPositionInNode(*foo), |
| result_in_flat_tree.Base()); |
| EXPECT_EQ(PositionInFlatTree::BeforeNode(*bar), result_in_flat_tree.Extent()); |
| |
| // Select from bar to foo in DOM tree. |
| const SelectionInDOMTree& selection2 = |
| SelectionInDOMTree::Builder() |
| .Collapse(Position::LastPositionInNode(*bar)) |
| .Extend(Position::FirstPositionInNode(*foo)) |
| .Build(); |
| const SelectionInDOMTree& result2 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection2); |
| |
| EXPECT_EQ(Position::LastPositionInNode(*bar), result2.Base()); |
| EXPECT_EQ(Position::FirstPositionInNode(*bar), result2.Extent()); |
| |
| // Select from bar to foo in flat tree. |
| const SelectionInFlatTree& selection_in_flat_tree2 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Extend(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Build(); |
| const SelectionInFlatTree& result_in_flat_tree2 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection_in_flat_tree2); |
| |
| EXPECT_EQ(PositionInFlatTree::LastPositionInNode(*bar), |
| result_in_flat_tree2.Base()); |
| EXPECT_EQ(PositionInFlatTree::FirstPositionInNode(*bar), |
| result_in_flat_tree2.Extent()); |
| } |
| |
| TEST_F(SelectionAdjusterTest, ShadowDistributedNodesWithoutEditingBoundary) { |
| const char* body_content = R"HTML( |
| <div id=host> |
| <div id=foo slot=foo>foo</div> |
| <div id=bar slot=bar>bar</div> |
| </div>)HTML"; |
| const char* shadow_content = R"HTML( |
| <div> |
| <div id=s1>111</div> |
| <slot name=foo></slot> |
| <div id=s2>222</div> |
| <slot name=bar></slot> |
| <div id=s3>333</div> |
| </div>)HTML"; |
| SetBodyContent(body_content); |
| Element* host = GetDocument().getElementById("host"); |
| ShadowRoot& shadow_root = |
| host->AttachShadowRootInternal(ShadowRootType::kOpen); |
| shadow_root.SetInnerHTMLFromString(shadow_content); |
| |
| Element* foo = GetDocument().getElementById("foo"); |
| Element* s1 = shadow_root.QuerySelector("#s1"); |
| |
| // Select from 111 to foo. |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*s1)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*foo)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| EXPECT_EQ(R"HTML( |
| <div id="host"> |
| <div> |
| <div id="s1">^111</div> |
| <slot name="foo"><div id="foo" slot="foo">foo|</div></slot> |
| <div id="s2">222</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| |
| // Select from foo to 111. |
| const SelectionInFlatTree& selection2 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::LastPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::FirstPositionInNode(*s1)) |
| .Build(); |
| const SelectionInFlatTree& result2 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection2); |
| EXPECT_EQ(R"HTML( |
| <div id="host"> |
| <div> |
| <div id="s1">|111</div> |
| <slot name="foo"><div id="foo" slot="foo">foo^</div></slot> |
| <div id="s2">222</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result2)); |
| } |
| |
| // This test is just recording the behavior of current implementation, can be |
| // changed. |
| TEST_F(SelectionAdjusterTest, ShadowDistributedNodesWithEditingBoundary) { |
| const char* body_content = R"HTML( |
| <div contenteditable id=host> |
| <div id=foo slot=foo>foo</div> |
| <div id=bar slot=bar>bar</div> |
| </div>)HTML"; |
| const char* shadow_content = R"HTML( |
| <div> |
| <div id=s1>111</div> |
| <slot name=foo></slot> |
| <div id=s2>222</div> |
| <slot name=bar></slot> |
| <div id=s3>333</div> |
| </div>)HTML"; |
| SetBodyContent(body_content); |
| Element* host = GetDocument().getElementById("host"); |
| ShadowRoot& shadow_root = |
| host->AttachShadowRootInternal(ShadowRootType::kOpen); |
| shadow_root.SetInnerHTMLFromString(shadow_content); |
| |
| Element* foo = GetDocument().getElementById("foo"); |
| Element* bar = GetDocument().getElementById("bar"); |
| Element* s1 = shadow_root.QuerySelector("#s1"); |
| Element* s2 = shadow_root.QuerySelector("#s2"); |
| |
| // Select from 111 to foo. |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*s1)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*foo)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable id="host"> |
| <div> |
| <div id="s1">^111</div> |
| <slot name="foo">|<div id="foo" slot="foo">foo</div></slot> |
| <div id="s2">222</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| |
| // Select from foo to 111. |
| const SelectionInFlatTree& selection2 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::LastPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::FirstPositionInNode(*s1)) |
| .Build(); |
| const SelectionInFlatTree& result2 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection2); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable id="host"> |
| <div> |
| <div id="s1">111</div> |
| <slot name="foo"><div id="foo" slot="foo">|foo^</div></slot> |
| <div id="s2">222</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result2)); |
| |
| // Select from 111 to 222. |
| const SelectionInFlatTree& selection3 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*s1)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*s2)) |
| .Build(); |
| const SelectionInFlatTree& result3 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection3); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable id="host"> |
| <div> |
| <div id="s1">^111</div> |
| <slot name="foo"><div id="foo" slot="foo">foo</div></slot> |
| <div id="s2">222|</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result3)); |
| |
| // Select from foo to bar. |
| const SelectionInFlatTree& selection4 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInFlatTree& result4 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection4); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable id="host"> |
| <div> |
| <div id="s1">111</div> |
| <slot name="foo"><div id="foo" slot="foo">^foo|</div></slot> |
| <div id="s2">222</div> |
| <slot name="bar"><div id="bar" slot="bar">bar</div></slot> |
| <div id="s3">333</div> |
| </div></div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result4)); |
| } |
| |
| TEST_F(SelectionAdjusterTest, EditingBoundaryOutsideOfShadowTree) { |
| SetBodyContent(R"HTML( |
| <div> |
| <div id=base>base</div> |
| <div id=div1 contenteditable> |
| 55 |
| <div id=host></div> |
| </div> |
| </div>)HTML"); |
| ShadowRoot* shadow_root = |
| SetShadowContent("<div id=extent>extent</div>", "host"); |
| Element* base = GetDocument().getElementById("base"); |
| Element* extent = shadow_root->QuerySelector("#extent"); |
| |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*base)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*extent)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| EXPECT_EQ(R"HTML( |
| <div> |
| <div id="base">^base</div> |
| |<div contenteditable id="div1"> |
| 55 |
| <div id="host"><div id="extent">extent</div></div> |
| </div> |
| </div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| } |
| |
| TEST_F(SelectionAdjusterTest, EditingBoundaryInsideOfShadowTree) { |
| SetBodyContent(R"HTML( |
| <div> |
| <div id=base>base</div> |
| <div id=host>foo</div> |
| </div>)HTML"); |
| ShadowRoot* shadow_root = SetShadowContent(R"HTML( |
| <div> |
| <div>bar</div> |
| <div contenteditable id=extent>extent</div> |
| <div>baz</div> |
| </div>)HTML", |
| "host"); |
| |
| Element* base = GetDocument().getElementById("base"); |
| Element* extent = shadow_root->QuerySelector("#extent"); |
| |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*base)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*extent)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| EXPECT_EQ(R"HTML( |
| <div> |
| <div id="base">^base</div> |
| <div id="host"> |
| <div> |
| <div>bar</div> |
| |<div contenteditable id="extent">extent</div> |
| <div>baz</div> |
| </div></div> |
| </div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| } |
| |
| // The current behavior of shadow host and shadow tree are editable is we can't |
| // cross the shadow boundary. |
| TEST_F(SelectionAdjusterTest, ShadowHostAndShadowTreeAreEditable) { |
| SetBodyContent(R"HTML( |
| <div contenteditable> |
| <div id=foo>foo</div> |
| <div id=host></div> |
| </div>)HTML"); |
| ShadowRoot* shadow_root = |
| SetShadowContent("<div contenteditable id=bar>bar</div>", "host"); |
| |
| Element* foo = GetDocument().getElementById("foo"); |
| Element* bar = shadow_root->QuerySelector("#bar"); |
| |
| // Select from foo to bar. |
| const SelectionInFlatTree& selection = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Extend(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Build(); |
| const SelectionInFlatTree& result = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable> |
| <div id="foo">^foo</div> |
| <div id="host">|<div contenteditable id="bar">bar</div></div> |
| </div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result)); |
| |
| // Select from bar to foo. |
| const SelectionInFlatTree& selection2 = |
| SelectionInFlatTree::Builder() |
| .Collapse(PositionInFlatTree::LastPositionInNode(*bar)) |
| .Extend(PositionInFlatTree::FirstPositionInNode(*foo)) |
| .Build(); |
| const SelectionInFlatTree& result2 = |
| SelectionAdjuster::AdjustSelectionToAvoidCrossingEditingBoundaries( |
| selection2); |
| EXPECT_EQ(R"HTML( |
| <div contenteditable> |
| <div id="foo">foo</div> |
| <div id="host"><div contenteditable id="bar">|bar^</div></div> |
| </div>)HTML", |
| GetSelectionTextInFlatTreeFromBody(result2)); |
| } |
| } // namespace blink |