blob: e5d4ef50e0756b9f9460756fcb9df377f07f2bdd [file]
// Copyright 2025 The Chromium Authors
// 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/annotation/node_annotation_selector.h"
#include "base/functional/callback.h"
#include "base/test/test_future.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/annotation/annotation_selector.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/position_with_affinity.h"
#include "third_party/blink/renderer/core/editing/range_in_flat_tree.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
class NodeAnnotationSelectorTest : public RenderingTest {
public:
NodeAnnotationSelectorTest()
: RenderingTest(MakeGarbageCollected<SingleChildLocalFrameClient>()) {}
void SetUp() override { RenderingTest::SetUp(); }
Range* DocumentBodyRange() {
Range* range = GetDocument().createRange();
range->selectNode(GetDocument().body());
return range;
}
};
TEST_F(NodeAnnotationSelectorTest, ConstructorAndSerialize) {
DOMNodeId test_id = 123;
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(test_id);
EXPECT_EQ(selector->Serialize(), "123");
}
TEST_F(NodeAnnotationSelectorTest, IsTextSelectorReturnsTrue) {
DOMNodeId test_id = 456;
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(test_id);
EXPECT_FALSE(selector->IsTextSelector());
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeFound) {
SetBodyInnerHTML(R"HTML(
<div id="first_div">Hello</div>
)HTML");
Element* div_element =
GetDocument().getElementById(AtomicString("first_div"));
DOMNodeId target_node_id = div_element->GetDomNodeId();
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(target_node_id);
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
ASSERT_NE(range_result, nullptr);
EXPECT_FALSE(range_result->IsNull());
PositionInFlatTree start_position = range_result->StartPosition();
PositionInFlatTree end_position = range_result->EndPosition();
EXPECT_EQ(start_position.AnchorNode(), div_element);
EXPECT_EQ(start_position.OffsetInContainerNode(), 0);
EXPECT_EQ(end_position.AnchorNode(), div_element);
EXPECT_EQ(end_position.AnchorType(), PositionAnchorType::kAfterChildren);
EXPECT_EQ(PlainText(range_result->ToEphemeralRange()), "Hello");
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeFoundMultipleTextNodes) {
SetBodyInnerHTML(R"HTML(
Other text to make sure we are not returning the whole document.
<div id="first_div">
<p>Hello</p><p>World</p>
</div>
Other text to make sure we are not returning the whole document.
)HTML");
Element* div_element =
GetDocument().getElementById(AtomicString("first_div"));
DOMNodeId target_node_id = div_element->GetDomNodeId();
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(target_node_id);
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
ASSERT_NE(range_result, nullptr);
EXPECT_FALSE(range_result->IsNull());
PositionInFlatTree start_position = range_result->StartPosition();
PositionInFlatTree end_position = range_result->EndPosition();
EXPECT_EQ(start_position.AnchorNode(), div_element);
EXPECT_EQ(start_position.OffsetInContainerNode(), 0);
EXPECT_EQ(end_position.AnchorNode(), div_element);
EXPECT_EQ(end_position.AnchorType(), PositionAnchorType::kAfterChildren);
EXPECT_EQ(PlainText(range_result->ToEphemeralRange()),
String("Hello\n\nWorld\n\n"));
}
TEST_F(NodeAnnotationSelectorTest,
FindRangeNodeFoundMultipleTextNodesAndImageInBetween) {
SetBodyInnerHTML(R"HTML(
Other text to make sure we are not returning the whole document.
<span id="first_span">
<p>Hello</p>
<img src="nonexistent.gif" width="42" height="42"
style="border:5px solid black">
<p>World</p>
</span>
Other text to make sure we are not returning the whole document.
)HTML");
Element* span_element =
GetDocument().getElementById(AtomicString("first_span"));
DOMNodeId target_node_id = span_element->GetDomNodeId();
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(target_node_id);
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
ASSERT_NE(range_result, nullptr);
EXPECT_FALSE(range_result->IsNull());
PositionInFlatTree start_position = range_result->StartPosition();
PositionInFlatTree end_position = range_result->EndPosition();
EXPECT_EQ(start_position.AnchorNode(), span_element);
EXPECT_EQ(start_position.OffsetInContainerNode(), 0);
EXPECT_EQ(end_position.AnchorNode(), span_element);
EXPECT_EQ(end_position.AnchorType(), PositionAnchorType::kAfterChildren);
EXPECT_EQ(PlainText(range_result->ToEphemeralRange()),
String("Hello\n\n\nWorld\n\n"));
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeFoundButNoText) {
SetBodyInnerHTML(R"HTML(
Other text to make sure we are not returning the whole document.
Also add white space and new lines only in the div to make sure
we're not selecting only empty space.
<div id="first_div">
</div>
Other text to make sure we are not returning the whole document.
)HTML");
Element* div_element =
GetDocument().getElementById(AtomicString("first_div"));
DOMNodeId target_node_id = div_element->GetDomNodeId();
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(target_node_id);
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
ASSERT_EQ(range_result, nullptr);
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeNotFound) {
SetBodyInnerHTML(R"HTML(
Other text to make sure we are not returning the whole document.
<div id="first_div">Hello</div>
Other text to make sure we are not returning the whole document.
)HTML");
Element* div_element =
GetDocument().getElementById(AtomicString("first_div"));
DOMNodeId target_node_id =
div_element->GetDomNodeId() + 9999; // a non-existent node id
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(target_node_id);
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
ASSERT_EQ(range_result, nullptr);
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeIsDisconnected) {
SetBodyInnerHTML(R"HTML(
Other text to make sure we are not returning the whole document.
<div id="first_div">
<p>Hello</p><p>World</p>
</div>
Other text to make sure we are not returning the whole document.
)HTML");
Element* div_element =
GetDocument().getElementById(AtomicString("first_div"));
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(div_element->GetDomNodeId());
div_element->remove();
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
EXPECT_EQ(range_result, nullptr);
}
TEST_F(NodeAnnotationSelectorTest, FindRangeNodeFromDifferentDocument) {
SetBodyInnerHTML(R"HTML(
<iframe></iframe>
<div>some text</div>
)HTML");
SetChildFrameHTML(R"HTML(
<div id="first_div">
<p>Hello</p><p>World</p>
</div>
)HTML");
Element* div_element =
ChildDocument().getElementById(AtomicString("first_div"));
ASSERT_NE(div_element->GetDocument(), GetDocument());
// Note that we use div_element's (which is in the iframe's document) ID, and
// the main frame's document.
NodeAnnotationSelector* selector =
MakeGarbageCollected<NodeAnnotationSelector>(div_element->GetDomNodeId());
base::test::TestFuture<const RangeInFlatTree*> future;
selector->FindRange(*DocumentBodyRange(),
AnnotationSelector::SearchType::kSynchronous,
future.GetCallback());
const RangeInFlatTree* range_result = future.Take();
EXPECT_EQ(range_result, nullptr);
}
} // namespace blink