| // 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_container_impl.h" |
| |
| #include <gtest/gtest.h> |
| |
| #include "base/run_loop.h" |
| #include "base/test/bind.h" |
| #include "components/shared_highlighting/core/common/shared_highlighting_metrics.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/core/annotation/annotation_agent_impl.h" |
| #include "third_party/blink/renderer/core/annotation/annotation_test_utils.h" |
| #include "third_party/blink/renderer/core/editing/finder/async_find_buffer.h" |
| #include "third_party/blink/renderer/core/editing/frame_selection.h" |
| #include "third_party/blink/renderer/core/editing/iterators/text_iterator.h" |
| #include "third_party/blink/renderer/core/editing/selection_template.h" |
| #include "third_party/blink/renderer/core/html/html_element.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/graphics/dom_node_id.h" |
| |
| namespace blink { |
| |
| class AnnotationAgentContainerImplTest : public SimTest { |
| public: |
| void SetUp() override { |
| SimTest::SetUp(); |
| WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600)); |
| } |
| |
| protected: |
| bool IsInContainer(AnnotationAgentImpl& agent, |
| AnnotationAgentContainerImpl& container) const { |
| return container.agents_.Contains(&agent); |
| } |
| |
| size_t GetAgentCount(AnnotationAgentContainerImpl& container) { |
| return container.agents_.size(); |
| } |
| |
| AnnotationAgentImpl* GetAgentAt(AnnotationAgentContainerImpl& container, |
| wtf_size_t index) { |
| return container.agents_[index]; |
| } |
| |
| void SendRightClick(const gfx::Point& click_point) { |
| auto event = frame_test_helpers::CreateMouseEvent( |
| WebMouseEvent::Type::kMouseDown, WebMouseEvent::Button::kRight, |
| click_point, /*modifiers=*/0); |
| event.click_count = 1; |
| WebView().MainFrameViewWidget()->HandleInputEvent( |
| WebCoalescedInputEvent(event, ui::LatencyInfo())); |
| } |
| |
| ScopedUseMockAnnotationSelector use_mock_annotation_selector_; |
| }; |
| |
| // Ensure containers aren't supplementing a document until they're requested. |
| TEST_F(AnnotationAgentContainerImplTest, IsConstructedLazily) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest child_request("https://example.com/subframe.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| TEST PAGE |
| <iframe src="https://example.com/subframe.html"></iframe> |
| )HTML"); |
| child_request.Complete(R"HTML( |
| <!DOCTYPE html> |
| SUBFRAME |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| ASSERT_TRUE(GetDocument().GetFrame()); |
| ASSERT_TRUE(GetDocument().GetFrame()->FirstChild()); |
| LocalFrame* child_frame = |
| DynamicTo<LocalFrame>(GetDocument().GetFrame()->FirstChild()); |
| ASSERT_TRUE(child_frame); |
| |
| Document* child_document = child_frame->GetDocument(); |
| ASSERT_TRUE(child_document); |
| |
| // Initially, the container supplement should not exist on either document. |
| EXPECT_FALSE(AnnotationAgentContainerImpl::FromIfExists(GetDocument())); |
| EXPECT_FALSE(AnnotationAgentContainerImpl::FromIfExists(*child_document)); |
| |
| // Calling the getter on the container should create the supplement but only |
| // for the child document. |
| auto* child_container = |
| AnnotationAgentContainerImpl::CreateIfNeeded(*child_document); |
| EXPECT_TRUE(child_container); |
| EXPECT_EQ(child_container, |
| AnnotationAgentContainerImpl::FromIfExists(*child_document)); |
| EXPECT_FALSE(AnnotationAgentContainerImpl::FromIfExists(GetDocument())); |
| |
| // Calling the getter for the main document should now create that supplement. |
| auto* main_container = |
| AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| EXPECT_EQ(main_container, |
| AnnotationAgentContainerImpl::FromIfExists(GetDocument())); |
| |
| // The child and main documents should each have their own containers. |
| EXPECT_NE(main_container, child_container); |
| } |
| |
| // Test that binding the mojo interface creates a new container. |
| TEST_F(AnnotationAgentContainerImplTest, BindingCreatesContainer) { |
| 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(); |
| |
| mojo::Remote<mojom::blink::AnnotationAgentContainer> remote; |
| ASSERT_FALSE(remote.is_bound()); |
| ASSERT_FALSE(AnnotationAgentContainerImpl::FromIfExists(GetDocument())); |
| |
| AnnotationAgentContainerImpl::BindReceiver( |
| GetDocument().GetFrame(), remote.BindNewPipeAndPassReceiver()); |
| |
| EXPECT_TRUE(remote.is_connected()); |
| EXPECT_TRUE(AnnotationAgentContainerImpl::FromIfExists(GetDocument())); |
| } |
| |
| // Test that navigating to a new document breaks the binding on the old |
| // document's container. |
| TEST_F(AnnotationAgentContainerImplTest, NavigationBreaksBinding) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest request_next("https://example.com/next.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| TEST PAGE |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| mojo::Remote<mojom::blink::AnnotationAgentContainer> remote; |
| AnnotationAgentContainerImpl::BindReceiver( |
| GetDocument().GetFrame(), remote.BindNewPipeAndPassReceiver()); |
| ASSERT_TRUE(remote.is_connected()); |
| |
| LoadURL("https://example.com/next.html"); |
| request_next.Complete(R"HTML( |
| <!DOCTYPE html> |
| NEXT PAGE |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| remote.FlushForTesting(); |
| |
| EXPECT_FALSE(remote.is_connected()); |
| } |
| |
| // Test that navigating to a new document installs a new container. |
| TEST_F(AnnotationAgentContainerImplTest, NavigationReplacesContainer) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest request_next("https://example.com/next.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()); |
| |
| LoadURL("https://example.com/next.html"); |
| request_next.Complete(R"HTML( |
| <!DOCTYPE html> |
| NEXT PAGE |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* container_next = |
| AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| EXPECT_NE(container, container_next); |
| } |
| |
| // Test that the container can create an unbound agent. |
| TEST_F(AnnotationAgentContainerImplTest, CreateUnboundAgent) { |
| 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_TRUE(agent); |
| EXPECT_FALSE(agent->IsBoundForTesting()); |
| EXPECT_FALSE(agent->IsAttached()); |
| |
| EXPECT_TRUE(IsInContainer(*agent, *container)); |
| } |
| |
| // Test that the container can create a bound agent. It should automatically |
| // perform attachment at creation time. |
| TEST_F(AnnotationAgentContainerImplTest, CreateBoundAgent) { |
| 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(); |
| |
| MockAnnotationAgentHost host; |
| auto remote_receiver_pair = host.BindForCreateAgent(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| ASSERT_TRUE(container); |
| container->CreateAgent( |
| std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| mojom::blink::AnnotationType::kSharedHighlight, |
| mojom::blink::Selector::NewSerializedSelector("MockAnnotationSelector")); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| // Creating an agent from selection should automatically attach, which will |
| // happen in the next BeginFrame. |
| Compositor().BeginFrame(); |
| host.FlushForTesting(); |
| EXPECT_TRUE(host.did_finish_attachment_rect_); |
| EXPECT_FALSE(host.did_disconnect_); |
| } |
| |
| // Test that creating an agent in a document that hasn't yet completed parsing |
| // will cause agents to defer attachment and attempt it when the document |
| // finishes parsing. |
| TEST_F(AnnotationAgentContainerImplTest, DeferAttachmentUntilFinishedParsing) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Write(R"HTML( |
| <!DOCTYPE html> |
| <body>TEST PAGE</body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| ASSERT_FALSE(GetDocument().HasFinishedParsing()); |
| |
| MockAnnotationAgentHost host; |
| auto remote_receiver_pair = host.BindForCreateAgent(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| ASSERT_TRUE(container); |
| container->CreateAgent( |
| std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| mojom::blink::AnnotationType::kSharedHighlight, |
| mojom::blink::Selector::NewSerializedSelector("MockAnnotationSelector")); |
| |
| // The agent should be created and bound. |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| // Attachment should not have been attempted yet. |
| host.FlushForTesting(); |
| EXPECT_FALSE(host.did_finish_attachment_rect_); |
| EXPECT_FALSE(host.did_disconnect_); |
| |
| request.Finish(); |
| ASSERT_TRUE(GetDocument().HasFinishedParsing()); |
| |
| // Now that parsing finished, attachment should be completed. |
| host.FlushForTesting(); |
| EXPECT_TRUE(host.did_finish_attachment_rect_); |
| } |
| |
| // Test that an agent removing itself also removes it from its container. |
| TEST_F(AnnotationAgentContainerImplTest, ManuallyRemoveAgent) { |
| 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* agent1 = container->CreateUnboundAgent( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| *MakeGarbageCollected<MockAnnotationSelector>()); |
| auto* agent2 = container->CreateUnboundAgent( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| *MakeGarbageCollected<MockAnnotationSelector>()); |
| |
| ASSERT_TRUE(agent1); |
| ASSERT_TRUE(agent2); |
| EXPECT_EQ(GetAgentCount(*container), 2ul); |
| EXPECT_TRUE(IsInContainer(*agent1, *container)); |
| EXPECT_TRUE(IsInContainer(*agent2, *container)); |
| |
| container->RemoveAgent(*agent1); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| EXPECT_FALSE(IsInContainer(*agent1, *container)); |
| EXPECT_TRUE(IsInContainer(*agent2, *container)); |
| |
| container->RemoveAgent(*agent2); |
| |
| EXPECT_EQ(GetAgentCount(*container), 0ul); |
| EXPECT_FALSE(IsInContainer(*agent2, *container)); |
| } |
| |
| // Test that navigating to a new document causes the agents to be disconnected |
| // from their hosts. |
| TEST_F(AnnotationAgentContainerImplTest, NavigationRemovesBoundAgents) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest request_next("https://example.com/next.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| TEST PAGE |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| MockAnnotationAgentHost host; |
| auto remote_receiver_pair = host.BindForCreateAgent(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| container->CreateAgent( |
| std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| mojom::blink::AnnotationType::kSharedHighlight, |
| mojom::blink::Selector::NewSerializedSelector("MockAnnotationSelector")); |
| ASSERT_EQ(GetAgentCount(*container), 1ul); |
| ASSERT_FALSE(host.did_disconnect_); |
| |
| LoadURL("https://example.com/next.html"); |
| request_next.Complete(R"HTML( |
| <!DOCTYPE html> |
| NEXT PAGE |
| )HTML"); |
| |
| host.FlushForTesting(); |
| EXPECT_TRUE(host.did_disconnect_); |
| } |
| |
| // Test that a detached document's container is no longer accessible. |
| TEST_F(AnnotationAgentContainerImplTest, |
| DetachedDocumentContainerBecomesInaccessible) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest request_next("https://example.com/next.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| TEST PAGE |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto& first_document = GetDocument(); |
| |
| LoadURL("https://example.com/next.html"); |
| request_next.Complete(R"HTML( |
| <!DOCTYPE html> |
| NEXT PAGE |
| )HTML"); |
| |
| EXPECT_FALSE(AnnotationAgentContainerImpl::CreateIfNeeded(first_document)); |
| } |
| |
| // When the document has no selection, calling CreateAgentFromSelection must |
| // not create an agent and it must return empty null bindings back to the |
| // caller. |
| TEST_F(AnnotationAgentContainerImplTest, |
| CreateAgentFromSelectionWithNoSelection) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body>TEST PAGE</body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| bool did_reply = false; |
| container->CreateAgentFromSelection( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| base::BindLambdaForTesting( |
| [&did_reply]( |
| mojom::blink::SelectorCreationResultPtr selector_creation_result, |
| shared_highlighting::LinkGenerationError error, |
| shared_highlighting::LinkGenerationReadyStatus ready_status) { |
| did_reply = true; |
| |
| EXPECT_FALSE(selector_creation_result); |
| EXPECT_EQ( |
| error, |
| shared_highlighting::LinkGenerationError::kEmptySelection); |
| // Test that the generation was not preemptive, the result was not |
| // ready by the time we called CreateAgentFromSelection. |
| EXPECT_EQ(ready_status, |
| shared_highlighting::LinkGenerationReadyStatus:: |
| kRequestedBeforeReady); |
| })); |
| |
| EXPECT_TRUE(did_reply); |
| EXPECT_EQ(GetAgentCount(*container), 0ul); |
| } |
| |
| // CreateAgentFromSelection must create an agent and return a selector for the |
| // selected text and bindings to the agent. It should also attach the agent to |
| // show the highlight. |
| TEST_F(AnnotationAgentContainerImplTest, |
| CreateAgentFromSelectionWithCollapsedSelection) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body>TEST PAGE</body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| FrameSelection& frame_selection = GetDocument().GetFrame()->Selection(); |
| |
| Element* body = GetDocument().body(); |
| frame_selection.SetSelection(SelectionInDomTree::Builder() |
| .Collapse(Position(body->firstChild(), 0)) |
| .Build(), |
| SetSelectionOptions()); |
| |
| bool did_reply = false; |
| container->CreateAgentFromSelection( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| base::BindLambdaForTesting( |
| [&did_reply]( |
| mojom::blink::SelectorCreationResultPtr selector_creation_result, |
| shared_highlighting::LinkGenerationError error, |
| shared_highlighting::LinkGenerationReadyStatus ready_status) { |
| did_reply = true; |
| |
| EXPECT_FALSE(selector_creation_result); |
| EXPECT_EQ( |
| error, |
| shared_highlighting::LinkGenerationError::kEmptySelection); |
| EXPECT_EQ(ready_status, |
| shared_highlighting::LinkGenerationReadyStatus:: |
| kRequestedBeforeReady); |
| })); |
| |
| EXPECT_TRUE(did_reply); |
| EXPECT_EQ(GetAgentCount(*container), 0ul); |
| } |
| |
| // CreateAgentFromSelection should synchronously return a preemptively generated |
| // result if one is available. |
| TEST_F(AnnotationAgentContainerImplTest, |
| CreateAgentFromSelectionWithPreemptiveGeneration) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body>TEST PAGE</body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| FrameSelection& frame_selection = GetDocument().GetFrame()->Selection(); |
| |
| Element* body = GetDocument().body(); |
| EphemeralRange range = EphemeralRange(Position(body->firstChild(), 0), |
| Position(body->firstChild(), 5)); |
| ASSERT_EQ("TEST ", PlainText(range)); |
| |
| frame_selection.SetSelection( |
| SelectionInDomTree::Builder().SetBaseAndExtent(range).Build(), |
| SetSelectionOptions()); |
| |
| // Right click on the selected text |
| const auto& selection_rect = CreateRange(range)->BoundingBox(); |
| SendRightClick(selection_rect.origin()); |
| |
| MockAnnotationAgentHost host; |
| |
| base::RunLoop run_loop; |
| |
| run_loop.RunUntilIdle(); |
| |
| bool did_reply = false; |
| container->CreateAgentFromSelection( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| base::BindLambdaForTesting( |
| [&did_reply, &host]( |
| mojom::blink::SelectorCreationResultPtr selector_creation_result, |
| shared_highlighting::LinkGenerationError error, |
| shared_highlighting::LinkGenerationReadyStatus ready_status) { |
| did_reply = true; |
| |
| EXPECT_EQ(selector_creation_result->selected_text, "TEST"); |
| EXPECT_EQ(selector_creation_result->serialized_selector, |
| "TEST,-PAGE"); |
| EXPECT_TRUE(selector_creation_result->host_receiver.is_valid()); |
| EXPECT_TRUE(selector_creation_result->agent_remote.is_valid()); |
| EXPECT_EQ(error, shared_highlighting::LinkGenerationError::kNone); |
| // Test that the generation was preemptive, the result was ready by |
| // the time we called CreateAgentFromSelection. |
| EXPECT_EQ(ready_status, |
| shared_highlighting::LinkGenerationReadyStatus:: |
| kRequestedAfterReady); |
| |
| host.Bind(std::move(selector_creation_result->host_receiver), |
| std::move(selector_creation_result->agent_remote)); |
| })); |
| |
| // Test that the callback from CreateAgentFromSelection invoked synchronously. |
| EXPECT_TRUE(did_reply); |
| |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| // Creating an agent from selection should automatically attach, which will |
| // happen in the next BeginFrame. |
| Compositor().BeginFrame(); |
| host.FlushForTesting(); |
| EXPECT_TRUE(host.did_finish_attachment_rect_); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| } |
| |
| // When the document has a collapsed selection, calling |
| // CreateAgentFromSelection must not create an agent and it must return empty |
| // null bindings back to the caller. |
| TEST_F(AnnotationAgentContainerImplTest, CreateAgentFromSelection) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body>TEST PAGE</body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| FrameSelection& frame_selection = GetDocument().GetFrame()->Selection(); |
| |
| Element* body = GetDocument().body(); |
| EphemeralRange range = EphemeralRange(Position(body->firstChild(), 0), |
| Position(body->firstChild(), 5)); |
| ASSERT_EQ("TEST ", PlainText(range)); |
| |
| frame_selection.SetSelection( |
| SelectionInDomTree::Builder().SetBaseAndExtent(range).Build(), |
| SetSelectionOptions()); |
| |
| const auto& selection_rect = CreateRange(range)->BoundingBox(); |
| SendRightClick(selection_rect.origin()); |
| |
| MockAnnotationAgentHost host; |
| |
| base::RunLoop run_loop; |
| container->CreateAgentFromSelection( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| base::BindLambdaForTesting( |
| [&run_loop, &host]( |
| mojom::blink::SelectorCreationResultPtr selector_creation_result, |
| shared_highlighting::LinkGenerationError error, |
| shared_highlighting::LinkGenerationReadyStatus ready_status) { |
| run_loop.Quit(); |
| |
| EXPECT_EQ(selector_creation_result->selected_text, "TEST"); |
| EXPECT_EQ(selector_creation_result->serialized_selector, |
| "TEST,-PAGE"); |
| EXPECT_TRUE(selector_creation_result->host_receiver.is_valid()); |
| EXPECT_TRUE(selector_creation_result->agent_remote.is_valid()); |
| EXPECT_EQ(error, shared_highlighting::LinkGenerationError::kNone); |
| // Test that the generation was preemptive, the result was ready by |
| // the time we called CreateAgentFromSelection. |
| EXPECT_EQ(ready_status, |
| shared_highlighting::LinkGenerationReadyStatus:: |
| kRequestedAfterReady); |
| |
| host.Bind(std::move(selector_creation_result->host_receiver), |
| std::move(selector_creation_result->agent_remote)); |
| })); |
| run_loop.Run(); |
| |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| // Creating an agent from selection should automatically attach, which will |
| // happen in the next BeginFrame. |
| Compositor().BeginFrame(); |
| host.FlushForTesting(); |
| EXPECT_TRUE(host.did_finish_attachment_rect_); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| } |
| |
| // Test that an in-progress, asynchronous generation is canceled gracefully if |
| // a new document is navigated. |
| TEST_F(AnnotationAgentContainerImplTest, ShutdownDocumentWhileGenerating) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| SimRequest request_next("https://example.com/next.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body> |
| <p>Multiple blocks</p> |
| <p>Multiple blocks</p> |
| <p>Multiple blocks</p> |
| <p>Multiple blocks</p> |
| <p>Multiple blocks</p> |
| <p id="target">TARGET</p> |
| <p>Multiple blocks</p> |
| </body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| // Set a tiny timeout so that the generator takes many tasks to finish its |
| // work. |
| auto auto_reset_timeout = |
| AsyncFindBuffer::OverrideTimeoutForTesting(base::TimeDelta::Min()); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| |
| FrameSelection& frame_selection = GetDocument().GetFrame()->Selection(); |
| |
| Element* target = GetDocument().getElementById(AtomicString("target")); |
| EphemeralRange range = |
| EphemeralRange(Position(target, 0), Position(target, 1)); |
| ASSERT_EQ("TARGET", PlainText(range)); |
| |
| frame_selection.SetSelection( |
| SelectionInDomTree::Builder().SetBaseAndExtent(range).Build(), |
| SetSelectionOptions()); |
| |
| // Right click on the selected text |
| const auto& selection_rect = CreateRange(range)->BoundingBox(); |
| SendRightClick(selection_rect.origin()); |
| |
| base::RunLoop run_loop; |
| bool did_finish = false; |
| |
| container->CreateAgentFromSelection( |
| mojom::blink::AnnotationType::kSharedHighlight, |
| base::BindLambdaForTesting( |
| [&did_finish]( |
| mojom::blink::SelectorCreationResultPtr selector_creation_result, |
| shared_highlighting::LinkGenerationError error, |
| shared_highlighting::LinkGenerationReadyStatus ready_status) { |
| did_finish = true; |
| EXPECT_FALSE(selector_creation_result); |
| EXPECT_EQ( |
| error, |
| shared_highlighting::LinkGenerationError::kIncorrectSelector); |
| // Test that the generation was preemptive, the result was ready by |
| // the time we called CreateAgentFromSelection. |
| EXPECT_EQ(ready_status, |
| shared_highlighting::LinkGenerationReadyStatus:: |
| kRequestedAfterReady); |
| })); |
| |
| // The above will have posted the first generator task to |
| // kInternalFindInPage. Post a task after it to exit back to test code after |
| // that task runs. |
| GetDocument() |
| .GetTaskRunner(TaskType::kInternalFindInPage) |
| ->PostTask(FROM_HERE, run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // The generator should still not have completed. Navigate the page to a new |
| // document. |
| EXPECT_FALSE(did_finish); |
| LoadURL("https://example.com/next.html"); |
| request_next.Complete(R"HTML( |
| <!DOCTYPE html> |
| NEXT PAGE |
| )HTML"); |
| |
| // The generation should complete but return failure, the agent should not |
| // have been created. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(did_finish); |
| EXPECT_EQ(GetAgentCount(*container), 0ul); |
| |
| // Ensure the new document doesn't somehow get involved. |
| auto* new_container = |
| AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| ASSERT_NE(new_container, container); |
| EXPECT_EQ(GetAgentCount(*new_container), 0ul); |
| } |
| |
| // Test that the container can create a bound agent using a node id selector. It |
| // should automatically perform attachment at creation time. |
| TEST_F(AnnotationAgentContainerImplTest, CreateBoundAgentWithNodeId) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML( |
| <!DOCTYPE html> |
| <body> |
| <p id="text">some text</p> |
| </body> |
| )HTML"); |
| Compositor().BeginFrame(); |
| |
| MockAnnotationAgentHost host; |
| auto remote_receiver_pair = host.BindForCreateAgent(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| ASSERT_TRUE(container); |
| container->CreateAgent( |
| std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| mojom::blink::AnnotationType::kSharedHighlight, |
| mojom::blink::Selector::NewNodeId(DOMNodeIds::IdForNode( |
| GetDocument().getElementById(AtomicString("text"))))); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| Compositor().BeginFrame(); |
| host.FlushForTesting(); |
| EXPECT_EQ(host.attachment_result_, mojom::blink::AttachmentResult::kSuccess); |
| EXPECT_FALSE(host.did_finish_attachment_rect_->IsEmpty()); |
| EXPECT_FALSE(host.did_disconnect_); |
| } |
| |
| TEST_F(AnnotationAgentContainerImplTest, |
| CreateBoundAgentWithNonExistentNodeId) { |
| SimRequest request("https://example.com/test.html", "text/html"); |
| LoadURL("https://example.com/test.html"); |
| request.Complete(R"HTML()HTML"); |
| Compositor().BeginFrame(); |
| |
| MockAnnotationAgentHost host; |
| auto remote_receiver_pair = host.BindForCreateAgent(); |
| |
| auto* container = AnnotationAgentContainerImpl::CreateIfNeeded(GetDocument()); |
| ASSERT_TRUE(container); |
| const int non_existent_node_id = 9999; |
| container->CreateAgent( |
| std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| mojom::blink::AnnotationType::kSharedHighlight, |
| mojom::blink::Selector::NewNodeId(non_existent_node_id)); |
| |
| EXPECT_EQ(GetAgentCount(*container), 1ul); |
| EXPECT_TRUE(host.agent_.is_connected()); |
| |
| Compositor().BeginFrame(); |
| host.FlushForTesting(); |
| EXPECT_EQ(host.attachment_result_, |
| mojom::blink::AttachmentResult::kSelectorNotMatched); |
| EXPECT_TRUE(host.did_finish_attachment_rect_->IsEmpty()); |
| EXPECT_FALSE(host.did_disconnect_); |
| } |
| |
| TEST_F(AnnotationAgentContainerImplTest, RemoveAgentsOfType) { |
| 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(); |
| |
| mojo::Remote<mojom::blink::AnnotationAgentContainer> remote; |
| AnnotationAgentContainerImpl::BindReceiver( |
| GetDocument().GetFrame(), remote.BindNewPipeAndPassReceiver()); |
| ASSERT_TRUE(remote.is_connected()); |
| auto* container = AnnotationAgentContainerImpl::FromIfExists(GetDocument()); |
| ASSERT_TRUE(container); |
| |
| struct AnnotationAgentHost { |
| MockAnnotationAgentHost host; |
| mojom::AnnotationType type = mojom::AnnotationType::kSharedHighlight; |
| }; |
| std::array<AnnotationAgentHost, 3> agent_hosts; |
| agent_hosts[0].type = mojom::AnnotationType::kGlic; |
| agent_hosts[2].type = mojom::AnnotationType::kGlic; |
| |
| for (auto& agent_host : agent_hosts) { |
| auto remote_receiver_pair = agent_host.host.BindForCreateAgent(); |
| container->CreateAgent(std::move(remote_receiver_pair.first), |
| std::move(remote_receiver_pair.second), |
| agent_host.type, |
| mojom::blink::Selector::NewSerializedSelector( |
| "MockAnnotationSelector")); |
| } |
| |
| // Make sure the agents are attached. |
| Compositor().BeginFrame(); |
| ASSERT_TRUE(GetAgentAt(*container, 0)->IsAttached()); |
| ASSERT_TRUE(GetAgentAt(*container, 1)->IsAttached()); |
| ASSERT_TRUE(GetAgentAt(*container, 2)->IsAttached()); |
| |
| remote->RemoveAgentsOfType(mojom::AnnotationType::kGlic); |
| remote.FlushForTesting(); |
| |
| // Only the agents of type kGlic should be removed. |
| EXPECT_EQ(GetAgentCount(*container), 1u); |
| EXPECT_EQ(GetAgentAt(*container, 0)->GetType(), |
| mojom::AnnotationType::kSharedHighlight); |
| |
| for (auto& agent_host : agent_hosts) { |
| agent_host.host.FlushForTesting(); |
| } |
| EXPECT_TRUE(agent_hosts[0].host.did_disconnect_); |
| EXPECT_FALSE(agent_hosts[1].host.did_disconnect_); |
| EXPECT_TRUE(agent_hosts[2].host.did_disconnect_); |
| } |
| |
| } // namespace blink |