blob: 1dcb395c33c50bc6d0ba1e2f62a3e06e198a6ea9 [file]
// Copyright 2022 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/annotation_agent_impl.h"
#include <gtest/gtest.h>
#include "base/containers/span.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "cc/base/features.h"
#include "components/shared_highlighting/core/common/fragment_directives_constants.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/mojom/annotation/annotation.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_font_face_descriptors.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_scroll_into_view_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview_string.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_boolean_scrollintoviewoptions.h"
#include "third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h"
#include "third_party/blink/renderer/core/annotation/annotation_test_utils.h"
#include "third_party/blink/renderer/core/annotation/node_annotation_selector.h"
#include "third_party/blink/renderer/core/annotation/text_annotation_selector.h"
#include "third_party/blink/renderer/core/css/font_face.h"
#include "third_party/blink/renderer/core/css/font_face_set_document.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/markers/glic_marker.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/range_in_flat_tree.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/root_frame_viewport.h"
#include "third_party/blink/renderer/core/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/geometry/dom_rect.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
namespace blink {
class AnnotationAgentImplTest : public SimTest {
public:
void SetUp() override {
SimTest::SetUp();
WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
}
protected:
AnnotationAgentImplTest()
: SimTest(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {
std::vector<base::test::FeatureRefAndParams> enabled_features = {
{
::features::kProgrammaticScrollAnimationOverride,
{
{"cubic_bezier_x1", "0.4"}, //
{"cubic_bezier_y1", "0.0"}, //
{"cubic_bezier_x2", "0.0"}, //
{"cubic_bezier_y2", "1.0"}, //
{"max_animation_duration", "1.5s"}, //
} //
} //
};
scoped_feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
std::move(enabled_features),
/*disabled_features=*/{});
}
// Helper to create a range to some text within a single text node. Verifies
// the Range selects the `expected` text.
RangeInFlatTree* CreateRangeToExpectedText(Node* node,
int start_offset,
int end_offset,
const String& expected) {
EXPECT_TRUE(node);
if (!node) {
return nullptr;
}
const auto& range_start = Position(node, start_offset);
const auto& range_end = Position(node, end_offset);
String actual = PlainText(EphemeralRange(range_start, range_end));
EXPECT_EQ(expected, actual);
if (expected != actual)
return nullptr;
return MakeGarbageCollected<RangeInFlatTree>(
ToPositionInFlatTree(range_start), ToPositionInFlatTree(range_end));
}
RangeInFlatTree* CreateRangeForWholeDocument(Document& document) {
const auto& range_start = PositionInFlatTree::FirstPositionInNode(document);
const auto& range_end = PositionInFlatTree::LastPositionInNode(document);
return MakeGarbageCollected<RangeInFlatTree>(
ToPositionInFlatTree(range_start), ToPositionInFlatTree(range_end));
}
// Returns the number of annotation markers that intersect the given range.
wtf_size_t NumMarkersInRange(RangeInFlatTree& range) {
return GetDocument()
.Markers()
.MarkersIntersectingRange(range.ToEphemeralRange(),
DocumentMarker::MarkerTypes::TextFragment())
.size();
}
// Creates an agent with a mock selector that will relect the given range
// when attached.
AnnotationAgentImpl* CreateAgentForRange(
RangeInFlatTree* range,
mojom::blink::AnnotationType type =
mojom::blink::AnnotationType::kSharedHighlight) {
EXPECT_TRUE(range);
if (!range)
return nullptr;
auto* container =
AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
EXPECT_TRUE(container);
if (!container)
return nullptr;
auto* mock_selector = MakeGarbageCollected<MockAnnotationSelector>(*range);
return container->CreateUnboundAgent(type, *mock_selector);
}
// Creates an agent with a mock selector that will always fail to find a
// range when attaching.
AnnotationAgentImpl* CreateAgentFailsAttach() {
auto* container =
AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
EXPECT_TRUE(container);
if (!container)
return nullptr;
auto* null_range = MakeGarbageCollected<RangeInFlatTree>();
DCHECK(null_range->IsNull());
auto* mock_selector =
MakeGarbageCollected<MockAnnotationSelector>(*null_range);
return container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight, *mock_selector);
}
// Creates an agent with a real text selector that will perform a real search
// of the DOM tree. Use a text selector string of the same format as a URL's
// text directive. (i.e. the part that comes after ":~:text=")
AnnotationAgentImpl* CreateTextFinderAgent(
const String& text_selector,
mojom::blink::AnnotationType type =
mojom::blink::AnnotationType::kSharedHighlight) {
auto* selector = MakeGarbageCollected<TextAnnotationSelector>(
TextFragmentSelector::FromTextDirective(text_selector));
auto* container =
AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
return container->CreateUnboundAgent(type, *selector);
}
AnnotationAgentImpl* CreateNodeAgent(
DOMNodeId dom_node_id,
mojom::blink::AnnotationType type = mojom::blink::AnnotationType::kGlic) {
auto* selector = MakeGarbageCollected<NodeAnnotationSelector>(dom_node_id);
auto* container =
AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
return container->CreateUnboundAgent(type, *selector);
}
// Performs a check that the given node is fully visible in the visual
// viewport - that is, it's entire bounding rect is contained in the visual
// viewport. Returns whether the check passed so it can be used as an ASSERT
// in tests.
bool ExpectInViewport(Node& node) {
VisualViewport& viewport =
GetDocument().View()->GetPage()->GetVisualViewport();
gfx::Rect rect_in_visual_viewport = viewport.RootFrameToViewport(
node.GetLayoutObject()->AbsoluteBoundingBoxRect(
kTraverseDocumentBoundaries));
gfx::Rect viewport_rect(viewport.Size());
bool is_contained = viewport_rect.Contains(rect_in_visual_viewport);
EXPECT_TRUE(is_contained)
<< "Expected [" << node.DebugName()
<< "] to be visible in viewport. Bounds relative to viewport: ["
<< rect_in_visual_viewport.ToString() << "] vs. viewport bounds [ "
<< viewport_rect.ToString() << " ]";
return is_contained;
}
// Opposite of above. Duplicated to provide correct error message when
// expectation fails.
bool ExpectNotInViewport(Node& node) {
VisualViewport& viewport =
GetDocument().View()->GetPage()->GetVisualViewport();
gfx::Rect rect_in_visual_viewport = viewport.RootFrameToViewport(
node.GetLayoutObject()->AbsoluteBoundingBoxRect(
kTraverseDocumentBoundaries));
gfx::Rect viewport_rect(viewport.Size());
bool is_contained = viewport_rect.Contains(rect_in_visual_viewport);
EXPECT_FALSE(is_contained)
<< "Expected [" << node.DebugName()
<< "] to be visible in viewport. Bounds relative to viewport: ["
<< rect_in_visual_viewport.ToString() << "] vs. viewport bounds [ "
<< viewport_rect.ToString() << " ]";
return !is_contained;
}
mojom::blink::AnnotationType GetAgentType(AnnotationAgentImpl* agent) {
return agent->type_;
}
bool IsRemoved(AnnotationAgentImpl* agent) { return agent->IsRemoved(); }
void LoadAhem() {
std::optional<Vector<char>> data =
test::ReadFromFile(test::CoreTestDataPath("Ahem.ttf"));
ASSERT_TRUE(data);
auto* buffer =
MakeGarbageCollected<V8UnionArrayBufferOrArrayBufferViewOrString>(
DOMArrayBuffer::Create(base::as_byte_span(*data)));
FontFace* ahem = FontFace::Create(GetDocument().GetFrame()->DomWindow(),
AtomicString("Ahem"), buffer,
FontFaceDescriptors::Create());
ScriptState* script_state =
ToScriptStateForMainWorld(GetDocument().GetFrame());
DummyExceptionStateForTesting exception_state;
FontFaceSetDocument::From(GetDocument())
->addForBinding(script_state, ahem, exception_state);
}
DocumentMarkerVector GetAllMarkers() {
return GetDocument().Markers().Markers();
}
float GetAlphaForGlicMarkerAt(wtf_size_t index) {
const auto& markers = GetAllMarkers();
EXPECT_GE(markers.size(), index + 1);
DocumentMarker* marker = markers[index];
EXPECT_EQ(marker->GetType(), DocumentMarker::MarkerType::kGlic);
return To<GlicMarker>(marker)->BackgroundColor().Alpha();
}
bool GlicAnimationRunning() {
return GetDocument().Markers().glic_animation_state_ ==
DocumentMarkerController::GlicAnimationState::kRunning;
}
bool GlicAnimationFinished() {
return GetDocument().Markers().glic_animation_state_ ==
DocumentMarkerController::GlicAnimationState::kFinished;
}
bool GlicAnimationNotStarted() {
return GetDocument().Markers().glic_animation_state_ ==
DocumentMarkerController::GlicAnimationState::kNotStarted;
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that the agent type is correctly set.
TEST_F(AnnotationAgentImplTest, AgentType) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* shared_highlight_agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
auto* glic_highlight_agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kGlic,
*MakeGarbageCollected<MockAnnotationSelector>());
EXPECT_EQ(GetAgentType(shared_highlight_agent),
mojom::blink::AnnotationType::kSharedHighlight);
EXPECT_EQ(GetAgentType(glic_highlight_agent),
mojom::blink::AnnotationType::kGlic);
}
// Ensure that simply creating an (unbound) agent doesn't automatically try to
// attach to DOM or bind to a mojo endpoint.
TEST_F(AnnotationAgentImplTest, CreatingDoesntBindOrAttach) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
EXPECT_FALSE(agent->IsBoundForTesting());
EXPECT_FALSE(agent->IsAttached());
}
// Tests that binding works.
TEST_F(AnnotationAgentImplTest, Bind) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
EXPECT_TRUE(host.agent_.is_connected());
EXPECT_FALSE(host.did_disconnect_);
}
// Tests that removing the agent disconnects bindings.
TEST_F(AnnotationAgentImplTest, RemoveDisconnectsBindings) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
ASSERT_TRUE(host.agent_.is_connected());
ASSERT_FALSE(host.did_disconnect_);
container->RemoveAgent(*agent);
host.FlushForTesting();
EXPECT_FALSE(host.agent_.is_connected());
EXPECT_TRUE(host.did_disconnect_);
}
// Tests that removing an agent clears all its state.
TEST_F(AnnotationAgentImplTest, RemoveClearsState) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
Compositor().BeginFrame();
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
EXPECT_FALSE(IsRemoved(agent));
Compositor().BeginFrame();
ASSERT_TRUE(agent->IsAttached());
container->RemoveAgent(*agent);
EXPECT_TRUE(IsRemoved(agent));
EXPECT_FALSE(agent->IsAttached());
}
// Tests that attaching an agent to DOM in the document happens in a BeginFrame.
TEST_F(AnnotationAgentImplTest, AttachDuringBeginFrame) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
TEST PAGE
)HTML");
Compositor().BeginFrame();
auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument());
ASSERT_TRUE(container);
auto* agent = container->CreateUnboundAgent(
mojom::blink::AnnotationType::kSharedHighlight,
*MakeGarbageCollected<MockAnnotationSelector>());
ASSERT_FALSE(agent->IsAttached());
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttached());
}
// Tests that attaching an agent to DOM will cause a document marker to be
// placed at the attached Range.
TEST_F(AnnotationAgentImplTest, SuccessfulAttachCreatesMarker) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range_foo = CreateRangeToExpectedText(p, 5, 8, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
RangeInFlatTree* range_bar = CreateRangeToExpectedText(p, 14, 17, "BAR");
auto* agent_bar = CreateAgentForRange(range_bar);
ASSERT_TRUE(agent_bar);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
ASSERT_TRUE(agent_bar->IsAttached());
// Both "FOO" and "BAR" should each have a single marker.
EXPECT_EQ(NumMarkersInRange(*range_foo), 1ul);
EXPECT_EQ(NumMarkersInRange(*range_bar), 1ul);
// Ensure we didn't create markers outside of the selected text.
RangeInFlatTree* range_test = CreateRangeToExpectedText(p, 0, 4, "TEST");
RangeInFlatTree* range_page = CreateRangeToExpectedText(p, 9, 13, "PAGE");
EXPECT_EQ(NumMarkersInRange(*range_test), 0ul);
EXPECT_EQ(NumMarkersInRange(*range_page), 0ul);
}
// Tests that removing an agent will cause its corresponding document marker to
// be removed as well.
TEST_F(AnnotationAgentImplTest, RemovedAgentRemovesMarkers) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range_foo = CreateRangeToExpectedText(p, 5, 8, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
RangeInFlatTree* range_bar = CreateRangeToExpectedText(p, 14, 17, "BAR");
auto* agent_bar = CreateAgentForRange(range_bar);
ASSERT_TRUE(agent_bar);
Compositor().BeginFrame();
ASSERT_EQ(NumMarkersInRange(*range_foo), 1ul);
ASSERT_EQ(NumMarkersInRange(*range_bar), 1ul);
AnnotationAgentContainerImpl* container = agent_foo->OwningContainer();
container->RemoveAgent(*agent_foo);
ASSERT_EQ(NumMarkersInRange(*range_foo), 0ul);
ASSERT_EQ(NumMarkersInRange(*range_bar), 1ul);
container->RemoveAgent(*agent_bar);
ASSERT_EQ(NumMarkersInRange(*range_foo), 0ul);
ASSERT_EQ(NumMarkersInRange(*range_bar), 0ul);
}
// Tests the case where an agent's selector fails to attach to any content in
// the DOM. Ensure markers aren't created but the agent remains in a live
// state.
TEST_F(AnnotationAgentImplTest, AgentFailsAttachment) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
auto* agent = CreateAgentFailsAttach();
ASSERT_TRUE(agent);
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range =
CreateRangeToExpectedText(p, 0, 17, "TEST FOO PAGE BAR");
ASSERT_EQ(NumMarkersInRange(*range), 0ul);
Compositor().BeginFrame();
EXPECT_EQ(NumMarkersInRange(*range), 0ul);
EXPECT_FALSE(agent->IsAttached());
EXPECT_FALSE(IsRemoved(agent));
}
// Tests that failing to attach is still reported to the host the attempt
// completes.
TEST_F(AnnotationAgentImplTest, AgentFailsAttachmentReportsToHost) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
auto* agent = CreateAgentFailsAttach();
ASSERT_TRUE(agent);
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
ASSERT_FALSE(host.did_disconnect_);
ASSERT_TRUE(host.agent_.is_connected());
ASSERT_FALSE(host.did_finish_attachment_rect_);
Compositor().BeginFrame();
host.FlushForTesting();
ASSERT_TRUE(host.did_finish_attachment_rect_);
EXPECT_TRUE(host.did_finish_attachment_rect_->IsEmpty());
}
// Tests that an overlapping marker still reports a completed attachment to the
// host.
TEST_F(AnnotationAgentImplTest, AttachmentToOverlappingMarkerReportsToHost) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
Node* text_node =
GetDocument().getElementById(AtomicString("text"))->firstChild();
auto* agent = CreateAgentFailsAttach();
ASSERT_TRUE(agent);
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(text_node, 5, 13, "FOO PAGE");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
RangeInFlatTree* range_bar =
CreateRangeToExpectedText(text_node, 9, 17, "PAGE BAR");
auto* agent_bar = CreateAgentForRange(range_bar);
ASSERT_TRUE(agent_bar);
MockAnnotationAgentHost host_foo;
MockAnnotationAgentHost host_bar;
host_foo.BindToAgent(*agent_foo);
host_bar.BindToAgent(*agent_bar);
ASSERT_FALSE(host_foo.did_finish_attachment_rect_);
ASSERT_FALSE(host_bar.did_finish_attachment_rect_);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
ASSERT_TRUE(agent_bar->IsAttached());
host_foo.FlushForTesting();
EXPECT_TRUE(host_foo.did_finish_attachment_rect_);
EXPECT_TRUE(host_bar.did_finish_attachment_rect_);
}
// Tests that attached agents report the document-coordinate rects of the
// ranges to the host.
TEST_F(AnnotationAgentImplTest, AttachmentReportsRectsToHost) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
#bar {
position: absolute;
top: 2000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
<p id='bar'>BAR</p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
// Zoom and scroll to non-default values so we can ensure the coordinates of
// the attached ranges are in the document coordinate space.
{
WebView().SetPageScaleFactor(2);
GetDocument().View()->GetRootFrameViewport()->SetScrollOffset(
ScrollOffset(123, 3000), mojom::blink::ScrollType::kProgrammatic,
cc::ScrollSourceType::kNone, mojom::blink::ScrollBehavior::kInstant);
// The visual viewport consumes all the horizontal scroll and 300px (its
// max scroll offset) of the vertical scroll.
VisualViewport& viewport =
GetDocument().View()->GetPage()->GetVisualViewport();
ASSERT_EQ(viewport.Scale(), 2.f);
ASSERT_EQ(viewport.GetScrollOffset(), ScrollOffset(123, 300));
ASSERT_EQ(GetDocument().View()->LayoutViewport()->GetScrollOffset(),
ScrollOffset(0, 2700));
}
Node* node_foo =
GetDocument().getElementById(AtomicString("foo"))->firstChild();
Node* node_bar =
GetDocument().getElementById(AtomicString("bar"))->firstChild();
RangeInFlatTree* range_foo = CreateRangeToExpectedText(node_foo, 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
RangeInFlatTree* range_bar = CreateRangeToExpectedText(node_bar, 0, 3, "BAR");
auto* agent_bar = CreateAgentForRange(range_bar);
ASSERT_TRUE(agent_bar);
MockAnnotationAgentHost host_foo;
MockAnnotationAgentHost host_bar;
host_foo.BindToAgent(*agent_foo);
host_bar.BindToAgent(*agent_bar);
ASSERT_FALSE(host_foo.did_finish_attachment_rect_);
ASSERT_FALSE(host_bar.did_finish_attachment_rect_);
Compositor().BeginFrame();
EXPECT_TRUE(agent_foo->IsAttached());
EXPECT_TRUE(agent_bar->IsAttached());
host_foo.FlushForTesting();
host_bar.FlushForTesting();
ASSERT_TRUE(host_foo.did_finish_attachment_rect_);
ASSERT_TRUE(host_bar.did_finish_attachment_rect_);
EXPECT_EQ(*host_foo.did_finish_attachment_rect_, gfx::Rect(0, 1010, 30, 10));
EXPECT_EQ(*host_bar.did_finish_attachment_rect_, gfx::Rect(0, 2010, 30, 10));
}
// Tests that calling ScrollIntoView will ensure the marker is in the viewport.
TEST_F(AnnotationAgentImplTest, AgentScrollIntoView) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Invoking ScrollIntoView on the agent should cause the attached content
// into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
// Tests that calling ScrollIntoView with `applies_focus` set to false will not
// focus the element or clear the focus.
TEST_F(AnnotationAgentImplTest, AgentScrollIntoViewDoesNotApplyFocus) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo' tabindex=0>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
ASSERT_FALSE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
nullptr);
// Invoking ScrollIntoView on the agent should cause the attached content
// into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_foo));
EXPECT_FALSE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
element_foo);
}
// Tests that calling ScrollIntoView with `applies_focus` set to true will
// ensure that the element will be focused if focusable.
TEST_F(AnnotationAgentImplTest, AgentScrollIntoViewAppliesFocusAndFocusable) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo' tabindex=0>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
ASSERT_FALSE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
nullptr);
// Invoking ScrollIntoView on the agent should cause the attached content
// into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/true);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_foo));
EXPECT_TRUE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
element_foo);
}
// Tests that calling ScrollIntoView with `applies_focus` set to true will
// ensure that the focus is cleared and the sequential tab navigation point is
// correctly set if the element is not focusable.
TEST_F(AnnotationAgentImplTest,
AgentScrollIntoViewAppliesFocusAndNotFocusable) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
ASSERT_FALSE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
nullptr);
// Invoking ScrollIntoView on the agent should cause the attached content
// into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/true);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_foo));
EXPECT_FALSE(element_foo->IsFocusedElementInDocument());
ASSERT_EQ(GetDocument().SequentialFocusNavigationStartingPoint(
mojom::blink::FocusType::kForward),
element_foo);
}
// Tests that calling ScrollIntoView will ensure the marker is in the viewport
// when the page has been pinch-zoomed.
TEST_F(AnnotationAgentImplTest, AgentScrollIntoViewZoomed) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 5000px;
left: 400px;
}
body {
margin: 0;
height: 10000px;
}
</style>
<p id='foo'>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
// The page is non-horizontally scrollable but pinch-zoom so that the "FOO"
// text is just off-screen on the right. This will ensure ScrollIntoView also
// moves the visual viewport if the user pinch-zoomed in.
WebView().SetPageScaleFactor(2);
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Invoking ScrollIntoView on the agent should cause the attached content
// into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
// Test that calling ScrollIntoView while layout is dirty causes the page to
// update layout and correctly ScrollIntoView the agent.
TEST_F(AnnotationAgentImplTest, ScrollIntoViewWithDirtyLayout) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
position: absolute;
top: 100px;
}
</style>
<p id='text'>FOO BAR</p>
)HTML");
Compositor().BeginFrame();
Element* element_text = GetDocument().getElementById(AtomicString("text"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_text->firstChild(), 0, 3, "FOO");
auto* agent_foo = CreateAgentForRange(range_foo);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectInViewport(*element_text));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
element_text->setAttribute(html_names::kStyleAttr,
AtomicString("top: 2000px"));
// Invoking ScrollIntoView on the agent should perform layout and then cause
// the attached content to scroll into the viewport.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
EXPECT_TRUE(ExpectInViewport(*element_text));
EXPECT_GT(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset().y(),
1000);
}
// Degenerate case but make sure it doesn't crash. This constructs a
// RangeInFlatTree that isn't collapsed but turns into a collapsed
// EphmemeralRangeInFlatTree.
TEST_F(AnnotationAgentImplTest, ScrollIntoViewCollapsedRange) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
position: absolute;
top: 2000px;
}
</style>
<p id='text'>a</p>
)HTML");
Compositor().BeginFrame();
Element* element_text = GetDocument().getElementById(AtomicString("text"));
const auto& range_start =
Position(element_text->firstChild(), PositionAnchorType::kBeforeAnchor);
const auto& range_end = Position(element_text, 0);
RangeInFlatTree* range = MakeGarbageCollected<RangeInFlatTree>(
ToPositionInFlatTree(range_start), ToPositionInFlatTree(range_end));
// TODO(bokan): Is this an editing bug?
ASSERT_FALSE(range->IsCollapsed());
ASSERT_TRUE(range->ToEphemeralRange().IsCollapsed());
auto* agent = CreateAgentForRange(range);
ASSERT_TRUE(agent);
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
Compositor().BeginFrame();
// Attachment should fail for this collapsed range.
EXPECT_FALSE(agent->IsAttached());
host.FlushForTesting();
// Ensure calling ScrollIntoView doesn't crash.
host.agent_->ScrollIntoView(/*applies_focus=*/false);
host.FlushForTesting();
EXPECT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset().y(),
0);
}
// Ensure an annotation causes a hidden <details> section to be opened when
// text inside it is attached.
TEST_F(AnnotationAgentImplTest, OpenDetailsElement) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
details {
position: absolute;
top: 2000px;
}
</style>
<details id='details'>foobar</p>
)HTML");
Compositor().BeginFrame();
Element* element_details =
GetDocument().getElementById(AtomicString("details"));
ASSERT_FALSE(element_details->FastHasAttribute(html_names::kOpenAttr));
auto* agent = CreateTextFinderAgent("foobar");
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
EXPECT_FALSE(agent->IsAttachmentPending());
Compositor().BeginFrame();
host.FlushForTesting();
// Since the matching text is inside a <details> it is initially hidden. The
// attachment will be asynchronous as the <details> element must be opened
// which needs to happen in a safe place during the document lifecycle.
EXPECT_TRUE(agent->IsAttachmentPending());
EXPECT_FALSE(agent->IsAttached());
EXPECT_FALSE(host.did_finish_attachment_rect_);
EXPECT_FALSE(element_details->FastHasAttribute(html_names::kOpenAttr));
// ScrollIntoView, if called, shouldn't cause a scroll yet.
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Produce a compositor frame. This should process the DOM mutations and
// finish attaching the agent.
Compositor().BeginFrame();
host.FlushForTesting();
EXPECT_TRUE(element_details->FastHasAttribute(html_names::kOpenAttr));
EXPECT_FALSE(agent->IsAttachmentPending());
EXPECT_TRUE(agent->IsAttached());
EXPECT_TRUE(host.did_finish_attachment_rect_);
// ScrollIntoView should now correctly scroll to the expanded details element.
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_GT(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset().y(),
100.f);
}
// Ensure an annotation causes a `hidden=until-found` section to be shown when
// text inside it is attached.
TEST_F(AnnotationAgentImplTest, OpenHiddenUntilFoundElement) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
position: absolute;
top: 2000px;
}
</style>
<p id="section" hidden="until-found">foobar</p>
)HTML");
Compositor().BeginFrame();
Element* element = GetDocument().getElementById(AtomicString("section"));
auto* agent = CreateTextFinderAgent("foobar");
Compositor().BeginFrame();
EXPECT_TRUE(element->FastHasAttribute(html_names::kHiddenAttr));
EXPECT_TRUE(agent->IsAttachmentPending());
// Produce a compositor frame. This should process the DOM mutations and
// finish attaching the agent.
Compositor().BeginFrame();
EXPECT_FALSE(element->FastHasAttribute(html_names::kHiddenAttr));
EXPECT_TRUE(agent->IsAttached());
}
// Ensure an annotation can target a content-visibility: auto section.
TEST_F(AnnotationAgentImplTest, ActivatesContentVisibilityAuto) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
position: absolute;
contain: strict;
width: 200px;
height: 20px;
left: 0;
top: 2000px;
content-visibility: auto;
}
</style>
<p id="section">foobar</p>
)HTML");
Compositor().BeginFrame();
auto* agent = CreateTextFinderAgent("foobar");
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttachmentPending());
// Produce a compositor frame. This should process the DOM mutations and
// finish attaching the agent.
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttached());
Node* node =
GetDocument().getElementById(AtomicString("section"))->firstChild();
RangeInFlatTree* range = CreateRangeToExpectedText(node, 0, 6, "foobar");
EXPECT_FALSE(DisplayLockUtilities::NeedsActivationForFindInPage(
range->ToEphemeralRange()));
}
TEST_F(AnnotationAgentImplTest, NodeContentsBeginsWithLineBreak) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
position: absolute;
width: 200px;
height: 20px;
left: 0;
top: 20000px;
}
</style>
<body>
<div id="section">
<div>Step</div>
1
</div>
</body>
)HTML");
auto* element_foo =
GetDocument().body()->getElementById(AtomicString("section"));
ASSERT_NE(element_foo, nullptr);
auto* agent = CreateNodeAgent(element_foo->GetDomNodeId());
// Produce a compositor frame. This should process the DOM mutations and
// finish attaching the agent.
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttached());
agent->ScrollIntoView(/*applies_focus=*/false);
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
TEST_F(AnnotationAgentImplTest, NodeWithDisplayContents) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#container {
position: absolute;
width: 200px;
height: 20px;
left: 0;
top: 20000px;
}
</style>
<body>
<div id="container">
<div id="contents" style="display: contents;">Text</div>
</div>
</body>
)HTML");
auto* div_with_display_contents =
GetDocument().body()->getElementById(AtomicString("contents"));
ASSERT_NE(div_with_display_contents, nullptr);
auto* agent = CreateNodeAgent(div_with_display_contents->GetDomNodeId());
// Produce a compositor frame. This should process the DOM mutations and
// finish attaching the agent.
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttached());
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_TRUE(ExpectInViewport(*div_with_display_contents->firstChild()));
}
// kTextFinder type annotations must not cause side-effects. Ensure they do not
// expand a hidden=until-found element.
TEST_F(AnnotationAgentImplTest, TextFinderDoesntMutateDom) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p hidden="until-found" id="text">TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
Element* p = GetDocument().getElementById(AtomicString("text"));
ASSERT_TRUE(p->FastHasAttribute(html_names::kHiddenAttr));
auto* agent_foo =
CreateTextFinderAgent("FOO", mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo);
Compositor().BeginFrame();
// Attachment should have succeeded but the <p> should remain hidden.
ASSERT_TRUE(agent_foo->IsAttached());
EXPECT_TRUE(p->FastHasAttribute(html_names::kHiddenAttr));
// Sanity check that a shared highlight does un-hide the <p>
auto* agent_bar = CreateTextFinderAgent(
"BAR", mojom::blink::AnnotationType::kSharedHighlight);
Compositor().BeginFrame();
ASSERT_TRUE(agent_bar->IsAttachmentPending());
Compositor().BeginFrame();
ASSERT_TRUE(agent_bar->IsAttached());
EXPECT_FALSE(p->FastHasAttribute(html_names::kHiddenAttr));
}
// kTextFinder type annotations must not cause side-effects. Ensure they do not
// create document markers.
TEST_F(AnnotationAgentImplTest, TextFinderDoesntAddMarkers) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id="text">TEST FOO PAGE BAR</p>
)HTML");
Compositor().BeginFrame();
RangeInFlatTree* doc_range = CreateRangeForWholeDocument(GetDocument());
ASSERT_EQ(NumMarkersInRange(*doc_range), 0ul);
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range_foo = CreateRangeToExpectedText(p, 5, 8, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo);
Compositor().BeginFrame();
// Attachment should have succeeded but no markers should be created.
EXPECT_EQ(NumMarkersInRange(*doc_range), 0ul);
// Sanity-check that a shared highlight does increase the marker count.
RangeInFlatTree* range_bar = CreateRangeToExpectedText(p, 14, 17, "BAR");
CreateAgentForRange(range_bar,
mojom::blink::AnnotationType::kSharedHighlight);
Compositor().BeginFrame();
EXPECT_EQ(NumMarkersInRange(*doc_range), 1ul);
}
// kTextFinder annotations should fail to find text within an empty
// overflow:hidden ancestor. This is a special case fix of
// https://crbug.com/1456392.
TEST_F(AnnotationAgentImplTest, TextFinderDoesntFindEmptyOverflowHidden) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<div id="container">
<p id="text">FOO BAR</p>
</div>
)HTML");
Compositor().BeginFrame();
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
Element* container = GetDocument().getElementById(AtomicString("container"));
RangeInFlatTree* range_foo = CreateRangeToExpectedText(p, 0, 3, "FOO");
// Empty container with `overflow: visible hidden` (y being hidden makes x
// compute to auto).
{
container->setAttribute(
html_names::kStyleAttr,
AtomicString("height: 0px; overflow: visible hidden"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// TextFinder should refuse to attach to the text since it has an empty,
// overflow: hidden ancestor.
EXPECT_FALSE(agent_foo->IsAttached());
}
// Empty container with `overflow: visible hidden` (y being hidden makes x
// compute to auto). TextFinder should refuse to attach to the text since
// it's clipped by the container.
{
container->setAttribute(
html_names::kStyleAttr,
AtomicString("height: 0px; overflow: visible hidden"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_FALSE(agent_foo->IsAttached());
}
// Empty container with `overflow: clip visible`. Should attach since
// `overflow: clip` can clip in a single axis and in this case is clipping
// the non-empty axis.
{
container->setAttribute(
html_names::kStyleAttr,
AtomicString("height: 0px; overflow: clip visible"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_TRUE(agent_foo->IsAttached());
}
// Empty container with clip on both axes. Shouldn't attach since it's
// clipped in the empty direction.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("height: 0px; overflow: clip clip"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_FALSE(agent_foo->IsAttached());
}
// Empty container with `overflow: visible clip`. Should fail since
// `overflow: clip` is in the empty direction
{
container->setAttribute(
html_names::kStyleAttr,
AtomicString("height: 0px; overflow: visible clip"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_FALSE(agent_foo->IsAttached());
}
// Giving the container size should make it visible to TextFinder annotations.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("height: 1px; overflow: hidden"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// Now that the ancestor has size TextFinder should attach.
EXPECT_TRUE(agent_foo->IsAttached());
}
// An empty container shouldn't prevent attaching if overflow is visible.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("height: 0px; overflow: visible"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// Now that the ancestor has size TextFinder should attach.
EXPECT_TRUE(agent_foo->IsAttached());
}
}
// kTextFinder annotations should fail to find text within an opacity:0
// subtree.
TEST_F(AnnotationAgentImplTest, TextFinderDoesntFindOpacityZero) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#container {
opacity: 0;
}
</style>
<div id="container">
<div>
<p id="text">FOO BAR</p>
</di>
</div>
)HTML");
Compositor().BeginFrame();
Node* p = GetDocument().getElementById(AtomicString("text"))->firstChild();
Element* container = GetDocument().getElementById(AtomicString("container"));
RangeInFlatTree* range_foo = CreateRangeToExpectedText(p, 0, 3, "FOO");
{
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// TextFinder should refuse to attach to the text since it has an opacity:
// 0 ancestor.
EXPECT_FALSE(agent_foo->IsAttached());
}
// Ensure that setting the opacity to a non-zero value makes it findable.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("opacity: 0.1"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_TRUE(agent_foo->IsAttached());
}
}
// kTextFinder annotations should fail to find text that's offscreen if it is
// in a position: fixed subtree.
TEST_F(AnnotationAgentImplTest, TextFinderDoesntFindOffscreenFixed) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#container {
position:fixed;
width: 100px;
height: 20px;
font: 10px/1 Ahem;
}
p {
position: relative;
margin: 0;
}
</style>
<div id="container">
<div>
<p id="text">FOO BAR</p>
</di>
</div>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* p = GetDocument().getElementById(AtomicString("text"));
Element* container = GetDocument().getElementById(AtomicString("container"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(p->firstChild(), 0, 3, "FOO");
// Ensure that putting the container offscreen makes the text unfindable.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("left: 0; top: -25px"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
EXPECT_FALSE(agent_foo->IsAttached());
}
// The container partially intersects the viewport but the range doesn't.
// This should still be considered unfindable.
{
container->setAttribute(html_names::kStyleAttr,
AtomicString("left: 0; top: -15px"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// Text is still offscreen.
ASSERT_LT(p->GetBoundingClientRect()->bottom(), 0);
EXPECT_FALSE(agent_foo->IsAttached());
}
// Push the <p> down so the text now intersects the viewport; this should
// make it findable.
{
p->setAttribute(html_names::kStyleAttr, AtomicString("top: 10px"));
auto* agent_foo = CreateAgentForRange(
range_foo, mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent_foo->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent_foo->NeedsAttachment());
// Text is now within the viewport.
ASSERT_GT(p->GetBoundingClientRect()->bottom(), 0);
EXPECT_TRUE(agent_foo->IsAttached());
}
}
TEST_F(AnnotationAgentImplTest, TextFinderFindsAcrossDisplayContents) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<body>
<div id="1">
<div id="2" style="display: contents;">
<span>Hello </span>
<span>world!</span>
</div>
</div>
</body>
)HTML");
Compositor().BeginFrame();
auto* agent = CreateTextFinderAgent("Hello%20world!",
mojom::blink::AnnotationType::kTextFinder);
ASSERT_TRUE(agent->NeedsAttachment());
Compositor().BeginFrame();
EXPECT_TRUE(agent->IsAttached());
}
TEST_F(AnnotationAgentImplTest, GlicShouldAnimateScroll) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Invoking ScrollIntoView on the agent will trigger a scroll, but the scroll
// will be animated (and so will not be in the viewport immediately).
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
EXPECT_TRUE(ExpectNotInViewport(*element_foo));
// Start and complete the animation (which should happen within 700 ms based
// on kDeltaBasedMaxDuration in scroll_offset_animation_curve.cc).
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
task_environment().FastForwardBy(base::Milliseconds(700));
Compositor().BeginFrame(/*time_delta_in_seconds*/ 0.7);
// The text should now be in the viewport.
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
TEST_F(AnnotationAgentImplTest, GlicShouldNotAnimateLongScroll) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 17000px;
}
body {
height: 20000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Invoking ScrollIntoView on the agent will trigger an instant scroll
// (without any animation), as the distance exceeds the threshold for a
// smooth scroll.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
// Tests that the device scale factor is taken into account when deciding
// whether to smooth scroll. A device with a higher DSF will have a higher
// threshold for smooth scrolling.
TEST_F(AnnotationAgentImplTest, GlicScrollConsidersDeviceScaleFactor) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 5000px;
}
body {
height: 10000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
WebView().MainFrameViewWidget()->SetDeviceScaleFactorForTesting(2.0);
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Invoking ScrollIntoView on the agent will trigger a scroll, but the scroll
// will be animated (and so will not be in the viewport immediately).
//
// The scroll distance will be ~10,000 physical pixels (due to the device
// scale factor), which would be above the threshold for a smooth scroll. But
// if we normalize the distance using the device scale factor (which we
// should), the scroll distance will be within the threshold.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
EXPECT_TRUE(ExpectNotInViewport(*element_foo));
// Start and complete the animation (which should happen within 1500 ms based
// on kProgrammaticScrollAnimationOverride's kMaxAnimationDuration in
// cc/base/features.cc).
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
task_environment().FastForwardBy(base::Milliseconds(700));
Compositor().BeginFrame(/*time_delta_in_seconds=*/1.5);
// The text should now be in the viewport.
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
// Tests that browser zoom doesn't affect the computation of scroll behaviour
// for mojom::blink::AnnotationType::kGlic.
TEST_F(AnnotationAgentImplTest, GlicScrollIgnoresBrowserZoom) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
p {
font: 10px/1 Ahem;
}
#foo {
position: absolute;
top: 10000px;
}
body {
height: 20000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
// Sets the zoom factor to (1.2 ^ 4)x (i.e. ~2x).
WebView().MainFrameViewWidget()->SetZoomLevelForTesting(4.0);
LoadAhem();
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
host_foo.FlushForTesting();
// Attachment must not cause any scrolling.
ASSERT_TRUE(ExpectNotInViewport(*element_foo));
ASSERT_EQ(GetDocument().View()->GetRootFrameViewport()->GetScrollOffset(),
ScrollOffset());
// Invoking ScrollIntoView on the agent will trigger an instant scroll
// (without any animation), as the distance exceeds the threshold for a
// smooth scroll. The scroll distance will be >10,000 physical pixels (due to
// browser zoom), which is above the threshold.
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_foo));
}
// Ensure that a range that's valid in a flat-tree ordering but invalid in
// regular DOM order doesn't cause a crash. https://crbug.com/410033683.
TEST_F(AnnotationAgentImplTest, ValidFlatTreeRangeIsInvalidDOMRange) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<html>
<head>
<script>
customElements.define(
'my-component',
class extends HTMLElement {
constructor() {
super();
const template = document.getElementById('my-component-template');
let templateContent = template.content;
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.appendChild(templateContent.cloneNode(true));
}
},
);
</script>
</head>
<body>
<template id="my-component-template">
<slot>
<p>
Temp
</p>
</slot>
<p>
After Slot
</p>
</template>
<my-component>
<!-- In a flat-tree traversal this node is treated as a child of the
<slot> element (inside <my-component>'s shadow root) but in a
regular tree traversal it is visited as part of the light-tree
children of the <my-component> element. This swaps its order with
respect to the "After Slot" <p>. -->
<p>
Match Text
</p>
</my-component>
</body>
</html>
)HTML");
LoadAhem();
Compositor().BeginFrame();
auto* agent = CreateTextFinderAgent("Match%20Text,After%20Slot");
ASSERT_TRUE(agent->NeedsAttachment());
Compositor().BeginFrame();
ASSERT_FALSE(agent->NeedsAttachment());
// Attachment will currently fail since DocumentMarkers doesn't support
// FlatTree ranges.
EXPECT_FALSE(agent->IsAttached());
}
// TODO(https://crbug.com/417493172): This should be parameterized for scroll
// types.
TEST_F(AnnotationAgentImplTest, GlicHighlight_SmokeTest) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST PAGE</p>
)HTML");
Compositor().BeginFrame();
Node* text_node =
GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range = CreateRangeToExpectedText(text_node, 0, 4, "TEST");
auto* agent = CreateAgentForRange(range, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent);
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
ASSERT_FALSE(host.did_finish_attachment_rect_);
// Finish the attachment.
Compositor().BeginFrame();
host.FlushForTesting();
ASSERT_TRUE(host.did_finish_attachment_rect_);
ASSERT_TRUE(agent->IsAttached());
EXPECT_TRUE(GlicAnimationNotStarted());
// The browser tells the renderer to scroll. Since we don't need to scroll,
// the highlight animation starts immediately, which queues the first
// RequestAnimationFrame.
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_TRUE(GlicAnimationRunning());
// Execute the first RequestAnimationFrame, which initializes the T=0.
Compositor().BeginFrame();
// T=500ms. `FastForwardBy()` makes sure we skip the `sleep()` inside
// `BeginFrame()`.
task_environment().FastForwardBy(base::Milliseconds(500));
Compositor().BeginFrame(0.5);
EXPECT_TRUE(GlicAnimationRunning());
// T=1100ms. Submit the last frame with progress=1.
task_environment().FastForwardBy(base::Milliseconds(600));
Compositor().BeginFrame(0.6);
EXPECT_TRUE(GlicAnimationFinished());
const auto& markers = GetAllMarkers();
EXPECT_EQ(markers.size(), 1u);
EXPECT_EQ(markers[0]->GetType(), DocumentMarker::MarkerType::kGlic);
EXPECT_EQ(
To<GlicMarker>(markers[0].Get())->BackgroundColor(),
Color::FromRGBA32(shared_highlighting::kFragmentTextBackgroundColorARGB));
}
// Test that the highlight animation is stopped when the agent is removed from
// the container. This can happen when the client dispose the agent.
TEST_F(AnnotationAgentImplTest, GlicHighlight_StopOnAgentRemoval) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST PAGE</p>
)HTML");
Compositor().BeginFrame();
Node* text_node =
GetDocument().getElementById(AtomicString("text"))->firstChild();
RangeInFlatTree* range = CreateRangeToExpectedText(text_node, 0, 4, "TEST");
auto* agent = CreateAgentForRange(range, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent);
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
ASSERT_FALSE(host.did_finish_attachment_rect_);
Compositor().BeginFrame();
host.FlushForTesting();
ASSERT_TRUE(host.did_finish_attachment_rect_);
ASSERT_TRUE(agent->IsAttached());
// The browser tells the renderer to scroll. Since we don't need to scroll,
// the highlight animation starts immediately, which queues the first
// RequestAnimationFrame.
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_EQ(GetAllMarkers().size(), 1u);
agent->OwningContainer()->RemoveAgent(*agent);
Compositor().BeginFrame();
EXPECT_TRUE(GetAllMarkers().empty());
}
// Regression test to make sure nothing crashes if the highlighted text node is
// removed during the animation.
TEST_F(AnnotationAgentImplTest, GlicHighlight_HighlightAfterTextRemoval) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>THE <span id='span'>TEST </span>PAGE</p>
)HTML");
Compositor().BeginFrame();
RangeInFlatTree* text_range = CreateRangeForWholeDocument(GetDocument());
auto* test_agent =
CreateAgentForRange(text_range, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(test_agent);
MockAnnotationAgentHost test_host;
test_host.BindToAgent(*test_agent);
ASSERT_FALSE(test_host.did_finish_attachment_rect_);
Compositor().BeginFrame();
test_host.FlushForTesting();
ASSERT_TRUE(test_host.did_finish_attachment_rect_);
GetDocument().getElementById(AtomicString{"span"})->remove();
Compositor().BeginFrame();
Element* text_element = GetDocument().getElementById(AtomicString{"text"});
EXPECT_EQ(GetDocument()
.Markers()
.MarkersFor(*To<Text>(text_element->firstChild()))
.size(),
1u);
EXPECT_EQ(
GetDocument()
.Markers()
.MarkersFor(*To<Text>(text_element->firstChild()->nextSibling()))
.size(),
1u);
text_element->remove();
Compositor().BeginFrame();
// The markers are kept alive despite all the text nodes are removed.
EXPECT_EQ(GetAllMarkers().size(), 3u);
}
// Test that the removal of a different agent does not affect the glic highlight
// agent.
TEST_F(AnnotationAgentImplTest, GlicHighlight_MultipleAgentRemoval) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text'>TEST PAGE</p>
)HTML");
Compositor().BeginFrame();
Element* element_text = GetDocument().getElementById(AtomicString("text"));
RangeInFlatTree* range =
CreateRangeToExpectedText(element_text->firstChild(), 0, 4, "TEST");
auto* glic_agent =
CreateAgentForRange(range, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(glic_agent);
auto* user_note_agent = CreateAgentForRange(
range, mojom::blink::AnnotationType::kSharedHighlight);
ASSERT_TRUE(user_note_agent);
MockAnnotationAgentHost glic_host;
glic_host.BindToAgent(*glic_agent);
MockAnnotationAgentHost user_note_host;
user_note_host.BindToAgent(*user_note_agent);
Compositor().BeginFrame();
glic_host.FlushForTesting();
user_note_host.FlushForTesting();
ASSERT_TRUE(user_note_agent->IsAttached());
ASSERT_TRUE(glic_agent->IsAttached());
EXPECT_EQ(GetAllMarkers().size(), 2u);
AnnotationAgentContainerImpl* container =
AnnotationAgentContainerImpl::FromIfExists(GetDocument());
ASSERT_TRUE(container);
container->RemoveAgent(*user_note_agent);
const auto& markers = GetAllMarkers();
ASSERT_EQ(markers.size(), 1u);
EXPECT_EQ(markers[0]->GetType(), DocumentMarker::MarkerType::kGlic);
container->RemoveAgent(*glic_agent);
EXPECT_TRUE(GetAllMarkers().empty());
}
// Test that we always start the highlight animation on a clean state.
TEST_F(AnnotationAgentImplTest, GlicHighlight_ResetStateOnNewTextNodes) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<p id='text1'>TEST </p>
<p id='text2'>PAGE</p>
)HTML");
Compositor().BeginFrame();
Element* text1 = GetDocument().getElementById(AtomicString("text1"));
RangeInFlatTree* range1 =
CreateRangeToExpectedText(text1->firstChild(), 0, 4, "TEST");
auto* agent1 =
CreateAgentForRange(range1, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent1);
MockAnnotationAgentHost host1;
host1.BindToAgent(*agent1);
Compositor().BeginFrame();
host1.FlushForTesting();
ASSERT_TRUE(agent1->IsAttached());
// The browser tells the renderer to scroll. Since we don't need to scroll,
// the highlight animation starts immediately, which queues the first
// RequestAnimationFrame.
agent1->ScrollIntoView(/*applies_focus=*/false);
// Execute the first RequestAnimationFrame with T=0.
Compositor().BeginFrame();
// Execute the first RequestAnimationFrame with T=0.5.
task_environment().FastForwardBy(base::Milliseconds(500));
Compositor().BeginFrame(0.5);
// Greater than 0.
EXPECT_GT(GetAlphaForGlicMarkerAt(0u), 0.f);
// Simulate that glic highlights a different text. Currently only one text
// (agent) is highlighted at a time.
agent1->OwningContainer()->RemoveAgent(*agent1);
EXPECT_TRUE(GetAllMarkers().empty());
// Add a second agent while the highlight from the first one is still
// animating (removing the agent doesn't stop the animation) its marker
// highlight. Ensure the new animation uses a fresh state.
Element* text2 = GetDocument().getElementById(AtomicString("text2"));
RangeInFlatTree* range2 =
CreateRangeToExpectedText(text2->firstChild(), 0, 4, "PAGE");
auto* agent2 =
CreateAgentForRange(range2, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent2);
MockAnnotationAgentHost host2;
host2.BindToAgent(*agent2);
Compositor().BeginFrame();
host2.FlushForTesting();
ASSERT_TRUE(agent2->IsAttached());
// No scroll. Queues the first RequestAnimationFrame.
agent2->ScrollIntoView(/*applies_focus=*/false);
// Execute the first RequestAnimationFrame with T=0.
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
EXPECT_EQ(GetAlphaForGlicMarkerAt(0u), 0.f);
}
TEST_F(AnnotationAgentImplTest,
GlicHighlight_HighLightStartsAfterScrollFinishes) {
ScopedEventTimingMatchingHTMLForTest feature_enabler(true);
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#foo {
position: absolute;
top: 1000px;
}
body {
height: 5000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
EXPECT_TRUE(GlicAnimationNotStarted());
// We need to scroll.
EXPECT_TRUE(ExpectNotInViewport(*element_foo));
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
Compositor().BeginFrame();
EXPECT_TRUE(GlicAnimationNotStarted());
// Smooth scrolling is guaranteed to finish within 1500ms.
task_environment().FastForwardBy(base::Seconds(2));
Compositor().BeginFrame(1.0);
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_foo));
// Since the text node is in the viewport, we must have queued the first
// RequestAnimationFrame.
EXPECT_TRUE(GlicAnimationRunning());
EXPECT_EQ(GetAlphaForGlicMarkerAt(0u), 0.f);
// Run the highlight animation.
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
EXPECT_TRUE(GlicAnimationRunning());
// The opacity is greater than 0.
EXPECT_GT(GetAlphaForGlicMarkerAt(0u), 0.f);
}
// Glic instant scrolls for more than 15000dip of distance.
TEST_F(AnnotationAgentImplTest, GlicHighlight_InstantStartForInstantScroll) {
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#foo {
position: absolute;
top: 17000px;
}
body {
height: 20000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML");
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
EXPECT_TRUE(GlicAnimationNotStarted());
// We need to scroll.
EXPECT_TRUE(ExpectNotInViewport(*element_foo));
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
// Instant scroll.
EXPECT_TRUE(ExpectInViewport(*element_foo));
EXPECT_TRUE(GlicAnimationRunning());
// Queue the first RequestAnimationFrame.
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
// Run the first RequestAnimationFrame.
task_environment().FastForwardBy(base::Milliseconds(16));
Compositor().BeginFrame();
EXPECT_TRUE(GlicAnimationRunning());
// The opacity is greater than 0.
EXPECT_GT(GetAlphaForGlicMarkerAt(0u), 0.f);
}
// Test that the highlight doesn't restart after subsequent scrollEnd events.
TEST_F(AnnotationAgentImplTest,
GlicHighlight_AnimationDoesNotRestartAfterSubsequentScroll) {
ScopedEventTimingMatchingHTMLForTest feature_enabler(true);
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(R"HTML(
<!DOCTYPE html>
<style>
#foo {
position: absolute;
top: 2000px;
}
#bar {
position: absolute;
top: 3000px;
}
body {
height: 4000px;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
<p id='bar'>BAR<p>
)HTML");
Compositor().BeginFrame();
Element* element_foo = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range_foo =
CreateRangeToExpectedText(element_foo->firstChild(), 0, 3, "FOO");
auto* agent_foo =
CreateAgentForRange(range_foo, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent_foo);
MockAnnotationAgentHost host_foo;
host_foo.BindToAgent(*agent_foo);
Compositor().BeginFrame();
ASSERT_TRUE(agent_foo->IsAttached());
EXPECT_TRUE(GlicAnimationNotStarted());
// We need to scroll.
EXPECT_TRUE(ExpectNotInViewport(*element_foo));
host_foo.agent_->ScrollIntoView(/*applies_focus=*/false);
host_foo.FlushForTesting();
Compositor().BeginFrame();
Compositor().BeginFrame();
EXPECT_TRUE(GlicAnimationNotStarted());
// Max smooth scrolling is capped at 1500ms.
task_environment().FastForwardBy(base::Seconds(2));
Compositor().BeginFrame(1.0);
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_foo));
// Since the text node is in the viewport, we must have queued the first
// RequestAnimationFrame.
EXPECT_TRUE(GlicAnimationRunning());
// Run the highlight animation, and finish it.
task_environment().FastForwardBy(base::Seconds(1));
Compositor().BeginFrame(/*time_delta_in_seconds=*/1);
EXPECT_TRUE(GlicAnimationFinished());
EXPECT_EQ(GetAlphaForGlicMarkerAt(0u), 1.f);
// Scroll BAR into the viewport.
Element* element_bar = GetDocument().getElementById(AtomicString("bar"));
EXPECT_TRUE(ExpectNotInViewport(*element_bar));
ScrollIntoViewOptions* options = ScrollIntoViewOptions::Create();
options->setBlock(V8ScrollLogicalPosition::Enum::kStart);
options->setBehavior(V8ScrollBehavior::Enum::kInstant);
auto* arg =
MakeGarbageCollected<V8UnionBooleanOrScrollIntoViewOptions>(options);
element_bar->scrollIntoViewForTesting(arg);
Compositor().BeginFrame();
EXPECT_TRUE(ExpectInViewport(*element_bar));
// If we started another highlight animation for scrolling BAR, the animation
// should not have restarted.
EXPECT_TRUE(GlicAnimationFinished());
EXPECT_EQ(GetAlphaForGlicMarkerAt(0u), 1.f);
}
namespace {
enum class ScrollType {
kNoScroll = 0,
kSmoothScroll,
kInstantScroll,
};
struct GlicScrollBehaviorConfig {
ScrollType type;
int element_top;
int body_height;
};
void PrintTo(const GlicScrollBehaviorConfig& v, std::ostream* os) {
std::string type;
switch (v.type) {
case ScrollType::kNoScroll:
type = "NoScroll";
break;
case ScrollType::kSmoothScroll:
type = "SmoothScroll";
break;
case ScrollType::kInstantScroll:
type = "InstantScroll";
break;
}
*os << type;
}
} // namespace
class AnnotationAgentImplTestWithScrollingBehavior
: public AnnotationAgentImplTest,
public ::testing::WithParamInterface<GlicScrollBehaviorConfig> {
public:
AnnotationAgentImplTestWithScrollingBehavior() = default;
~AnnotationAgentImplTestWithScrollingBehavior() override = default;
};
// Assert that there are highlight animations when prefers-reduced-motion is
// enabled, and SmoothScroll becomes instant.
TEST_P(AnnotationAgentImplTestWithScrollingBehavior,
NoAnimationWithPrefersReducedMotion) {
GlicScrollBehaviorConfig config = GetParam();
SimRequest request("https://example.com/test.html", "text/html");
LoadURL("https://example.com/test.html");
request.Complete(String::Format(R"HTML(
<!DOCTYPE html>
<style>
#foo {
position: absolute;
top: %dpx;
}
body {
height: %dpx;
margin: 0;
}
</style>
<p id='foo'>FOO<p>
)HTML", config.element_top, config.body_height)
);
Compositor().BeginFrame();
// Enables prefers-reduced-motion.
WebViewImpl* web_view =
GetDocument().GetPage()->GetChromeClient().GetWebView();
web_view->GetSettings()->SetPrefersReducedMotion(true);
Element* foo_element = GetDocument().getElementById(AtomicString("foo"));
RangeInFlatTree* range =
CreateRangeToExpectedText(foo_element->firstChild(), 0, 3, "FOO");
auto* agent = CreateAgentForRange(range, mojom::blink::AnnotationType::kGlic);
ASSERT_TRUE(agent);
MockAnnotationAgentHost host;
host.BindToAgent(*agent);
ASSERT_FALSE(host.did_finish_attachment_rect_);
// Finish the attachment.
Compositor().BeginFrame();
host.FlushForTesting();
ASSERT_TRUE(host.did_finish_attachment_rect_);
ASSERT_TRUE(agent->IsAttached());
ASSERT_TRUE(GlicAnimationNotStarted());
switch (GetParam().type) {
case ScrollType::kNoScroll: {
// The browser tells the renderer to scroll. Since we don't need to
// scroll, the highlight animation starts immediately, which sets the
// opacity to full.
agent->ScrollIntoView(/*applies_focus=*/false);
EXPECT_TRUE(ExpectInViewport(*foo_element));
break;
}
case ScrollType::kSmoothScroll:
// We would ordinarily smooth scroll here. Since prefers-reduced-motion is
// enabled, the scroll is instant.
case ScrollType::kInstantScroll: {
// We need to scroll.
EXPECT_TRUE(ExpectNotInViewport(*foo_element));
host.agent_->ScrollIntoView(/*applies_focus=*/false);
host.FlushForTesting();
// Instant scroll.
EXPECT_TRUE(ExpectInViewport(*foo_element));
break;
}
}
EXPECT_TRUE(GlicAnimationFinished());
EXPECT_EQ(GetAlphaForGlicMarkerAt(0u), 1.f);
}
INSTANTIATE_TEST_SUITE_P(
/*no prefix*/,
AnnotationAgentImplTestWithScrollingBehavior,
::testing::Values(
GlicScrollBehaviorConfig{.type = ScrollType::kNoScroll,
.element_top = 100,
.body_height = 200},
GlicScrollBehaviorConfig{.type = ScrollType::kSmoothScroll,
.element_top = 1000,
.body_height = 5000},
GlicScrollBehaviorConfig{.type = ScrollType::kInstantScroll,
.element_top = 17000,
.body_height = 20000}),
::testing::PrintToStringParamName());
} // namespace blink