chromium/third_party/blink/renderer/core/fragment_directive/text_fragment_anchor_test.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/containers/span.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/shared_highlighting/core/common/shared_highlighting_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_menu_source_type.h"
#include "third_party/blink/public/public_buildflags.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_font_face_descriptors.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_mouse_event_init.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_arraybuffer_arraybufferview_string.h"
#include "third_party/blink/renderer/core/annotation/annotation_agent_container_impl.h"
#include "third_party/blink/renderer/core/annotation/annotation_agent_impl.h"
#include "third_party/blink/renderer/core/css/font_face_set_document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/fragment_directive/text_fragment_finder.h"
#include "third_party/blink/renderer/core/fragment_directive/text_fragment_test_util.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/location.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h"
#include "third_party/blink/renderer/core/geometry/dom_rect.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/loader/empty_clients.h"
#include "third_party/blink/renderer/core/page/context_menu_controller.h"
#include "third_party/blink/renderer/core/page/scrolling/fragment_anchor.h"
#include "third_party/blink/renderer/core/paint/paint_layer_scrollable_area.h"
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/platform/scheduler/public/main_thread_scheduler.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.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"

#if BUILDFLAG(ENABLE_UNHANDLED_TAP)
#include "third_party/blink/public/mojom/unhandled_tap_notifier/unhandled_tap_notifier.mojom-blink.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#endif  // BUILDFLAG(ENABLE_UNHANDLED_TAP)

namespace blink {

namespace {

RunPendingTasks;

class TextFragmentAnchorTestController : public TextFragmentAnchorTestBase {};

class TextFragmentAnchorTest : public TextFragmentAnchorTestController {};

// Basic test case, ensure we scroll the matching text into view.
TEST_F(TextFragmentAnchorTest, BasicSmokeTest) {}

// Make sure an anchor isn't created (and we don't crash) if text= is empty.
TEST_F(TextFragmentAnchorTest, EmptyText) {}

// Make sure a non-matching string doesn't cause scroll and the fragment is
// removed when completed.
TEST_F(TextFragmentAnchorTest, NonMatchingString) {}

// Ensure multiple matches will scroll the first into view.
TEST_F(TextFragmentAnchorTest, MultipleMatches) {}

// Ensure matching works inside nested blocks.
TEST_F(TextFragmentAnchorTest, NestedBlocks) {}

// Ensure multiple texts are highlighted and the first is scrolled into
// view.
TEST_F(TextFragmentAnchorTest, MultipleTextFragments) {}

// Ensure we scroll the second text into view if the first isn't found.
TEST_F(TextFragmentAnchorTest, FirstTextFragmentNotFound) {}

// Ensure we still scroll the first text into view if the second isn't
// found.
TEST_F(TextFragmentAnchorTest, OnlyFirstTextFragmentFound) {}

// Make sure multiple non-matching strings doesn't cause scroll and the fragment
// is removed when completed.
TEST_F(TextFragmentAnchorTest, MultipleNonMatchingStrings) {}

// Test matching a text range within the same element
TEST_F(TextFragmentAnchorTest, SameElementTextRange) {}

// Test matching a text range across two neighboring elements
TEST_F(TextFragmentAnchorTest, NeighboringElementTextRange) {}

// Test matching a text range from an element to a deeper nested element
TEST_F(TextFragmentAnchorTest, DifferentDepthElementTextRange) {}

// Ensure that we don't match anything if endText is not found.
TEST_F(TextFragmentAnchorTest, TextRangeEndTextNotFound) {}

// Test matching multiple text ranges
TEST_F(TextFragmentAnchorTest, MultipleTextRanges) {}

// Ensure we scroll to the beginning of a text range larger than the viewport.
TEST_F(TextFragmentAnchorTest, DistantElementTextRange) {}

// Test a text range with both context terms in the same element.
TEST_F(TextFragmentAnchorTest, TextRangeWithContext) {}

// Ensure that we do not match a text range if the prefix is not found.
TEST_F(TextFragmentAnchorTest, PrefixNotFound) {}

// Ensure that we do not match a text range if the suffix is not found.
TEST_F(TextFragmentAnchorTest, SuffixNotFound) {}

// Test a text range with context terms in different elements
TEST_F(TextFragmentAnchorTest, TextRangeWithCrossElementContext) {}

// Test context terms separated by elements and whitespace
TEST_F(TextFragmentAnchorTest, CrossElementAndWhitespaceContext) {}

// Test context terms separated by empty sibling and parent elements
TEST_F(TextFragmentAnchorTest, CrossEmptySiblingAndParentElementContext) {}

// Ensure we scroll to text when its prefix and suffix are out of view.
TEST_F(TextFragmentAnchorTest, DistantElementContext) {}

// Test specifying just one of the prefix and suffix
TEST_F(TextFragmentAnchorTest, OneContextTerm) {}

class TextFragmentAnchorScrollTest
    : public TextFragmentAnchorTest,
      public testing::WithParamInterface<mojom::blink::ScrollType> {};

INSTANTIATE_TEST_SUITE_P();

// Test that a user scroll cancels the scroll into view.
TEST_P(TextFragmentAnchorScrollTest, ScrollCancelled) {}

// Test that user scrolling doesn't dismiss the highlight.
TEST_P(TextFragmentAnchorScrollTest, DontDismissTextHighlightOnUserScroll) {}

// Ensure that the text fragment anchor has no effect in an iframe. This is
// disabled in iframes by design, for security reasons.
TEST_F(TextFragmentAnchorTest, DisabledInIframes) {}

// Similarly to the iframe case, we also want to prevent activating a text
// fragment anchor inside a window.opened window.
TEST_F(TextFragmentAnchorTest, DisabledInWindowOpen) {}

// Ensure that the text fragment anchor is not activated by same-document script
// navigations.
TEST_F(TextFragmentAnchorTest, DisabledInSamePageNavigation) {}

// Ensure matching is case insensitive.
TEST_F(TextFragmentAnchorTest, CaseInsensitive) {}

// Test that the fragment anchor stays centered in view throughout loading.
TEST_F(TextFragmentAnchorTest, TargetStaysInView) {}

// Test that overlapping text ranges results in both highlights with
// a merged highlight.
TEST_F(TextFragmentAnchorTest, OverlappingTextRanges) {}

// Test matching a space to &nbsp character.
TEST_F(TextFragmentAnchorTest, SpaceMatchesNbsp) {}

// Test matching text with a CSS text transform.
TEST_F(TextFragmentAnchorTest, CSSTextTransform) {}

// Test that we scroll the element fragment into view if we don't find a match.
TEST_F(TextFragmentAnchorTest, NoMatchFoundFallsBackToElementFragment) {}

// Test that we don't match partial words at the beginning or end of the text.
TEST_F(TextFragmentAnchorTest, CheckForWordBoundary) {}

// Test that we don't match partial words with context
TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithContext) {}

// Test that we correctly match a whole word when it appears as a partial word
// earlier in the page.
TEST_F(TextFragmentAnchorTest, CheckForWordBoundaryWithPartialWord) {}

// Test click keeps the text highlight
TEST_F(TextFragmentAnchorTest, DismissTextHighlightWithClick) {}

// Test not dismissing the text highlight with a click.
TEST_F(TextFragmentAnchorTest, DontDismissTextHighlightWithClick) {}

// Test that a tap keeps the text highlight
TEST_F(TextFragmentAnchorTest, KeepsTextHighlightWithTap) {}

// Test not dismissing the text highlight with a tap.
TEST_F(TextFragmentAnchorTest, DontDismissTextHighlightWithTap) {}

// Test that we don't dismiss a text highlight before and after it's scrolled
// into view
TEST_F(TextFragmentAnchorTest, KeepsTextHighlightOutOfView) {}

// Test that a text highlight that didn't require a scroll into view is kept on
// tap
TEST_F(TextFragmentAnchorTest, KeepsTextHighlightInView) {}

// Test that the fragment directive delimiter :~: works properly and is stripped
// from the URL.
TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiter) {}

// Test that a :~: fragment directive is scrolled into view and is stripped from
// the URL when there's also a valid element fragment.
TEST_F(TextFragmentAnchorTest, FragmentDirectiveDelimiterWithElementFragment) {}

// Test that a fragment directive is stripped from the URL even if it is not a
// text directive.
TEST_F(TextFragmentAnchorTest, IdFragmentWithFragmentDirective) {}

// Ensure we can match <text> inside of a <svg> element.
TEST_F(TextFragmentAnchorTest, TextDirectiveInSvg) {}

// Ensure we restore the text highlight on page reload
// TODO(bokan): This test is disabled as this functionality was suppressed in
// https://crrev.com/c/2135407; it would be better addressed by providing a
// highlight-only function. See the TODO in
// https://wicg.github.io/ScrollToTextFragment/#restricting-the-text-fragment
TEST_F(TextFragmentAnchorTest, DISABLED_HighlightOnReload) {}

// Ensure that we can have text directives combined with non-text directives
TEST_F(TextFragmentAnchorTest, NonTextDirectives) {}

// Test that the text directive applies :target styling
TEST_F(TextFragmentAnchorTest, CssTarget) {}

// Ensure the text fragment anchor matching only occurs after the page becomes
// visible.
TEST_F(TextFragmentAnchorTest, PageVisibility) {}

// Regression test for https://crbug.com/1147568. Make sure a page setting
// manual scroll restoration doesn't cause the fragment to avoid scrolling on
// the initial load.
TEST_F(TextFragmentAnchorTest, ManualRestorationDoesntBlockFragment) {}

// Regression test for https://crbug.com/1147453. Ensure replaceState doesn't
// clobber the text fragment token and allows fragment to scroll.
TEST_F(TextFragmentAnchorTest, ReplaceStateDoesntBlockFragment) {}

// Test that a text directive can match across comment nodes
TEST_F(TextFragmentAnchorTest, MatchAcrossCommentNode) {}

// Test that selection is successful for same prefix and text start.
TEST_F(TextFragmentAnchorTest, SamePrefixAndText) {}

// Checks that selection in the same text node is considerered uninterrupted.
TEST_F(TextFragmentAnchorTest, IsInSameUninterruptedBlock_OneTextNode) {}

// Checks that selection in the same text node with nested non-block element is
// considerered uninterrupted.
TEST_F(TextFragmentAnchorTest,
       IsInSameUninterruptedBlock_NonBlockInterruption) {}

// Checks that selection in the same text node with nested block element is
// considerered interrupted.
TEST_F(TextFragmentAnchorTest, IsInSameUninterruptedBlock_BlockInterruption) {}

TEST_F(TextFragmentAnchorTest, OpenedFromHighlightDoesNotSelectAdditionalText) {}

// Test that on Android, a user can display a context menu by tapping on
// a text fragment, when the TextFragmentTapOpensContextMenu
// RuntimeEnabledFeature is enabled.
TEST_F(TextFragmentAnchorTest, ShouldOpenContextMenuOnTap) {}

#if BUILDFLAG(ENABLE_UNHANDLED_TAP)
// Mock implementation of the UnhandledTapNotifier Mojo receiver, for testing
// the ShowUnhandledTapUIIfNeeded notification.
class MockUnhandledTapNotifierImpl : public mojom::blink::UnhandledTapNotifier {
 public:
  MockUnhandledTapNotifierImpl() = default;

  void Bind(mojo::ScopedMessagePipeHandle handle) {
    receiver_.Bind(mojo::PendingReceiver<mojom::blink::UnhandledTapNotifier>(
        std::move(handle)));
  }

  void ShowUnhandledTapUIIfNeeded(
      mojom::blink::UnhandledTapInfoPtr unhandled_tap_info) override {
    was_unhandled_tap_ = true;
  }
  bool WasUnhandledTap() const { return was_unhandled_tap_; }
  bool ReceiverIsBound() const { return receiver_.is_bound(); }
  void Reset() {
    was_unhandled_tap_ = false;
    receiver_.reset();
  }

 private:
  bool was_unhandled_tap_ = false;

  mojo::Receiver<mojom::blink::UnhandledTapNotifier> receiver_{this};
};
#endif  // BUILDFLAG(ENABLE_UNHANDLED_TAP)

#if BUILDFLAG(ENABLE_UNHANDLED_TAP)
// Test that on Android, when a user taps on a text, ShouldNotRequestUnhandled
// does not get triggered. When a user taps on a highlight, no text should be
// selected. RuntimeEnabledFeature is enabled.
TEST_F(TextFragmentAnchorTest,
       ShouldNotRequestUnhandledTapNotifierWhenTapOnTextFragment) {
  LoadAhem();
  SimRequest request(
      "https://example.com/"
      "test.html#:~:text=this%20is%20a%20test%20page",
      "text/html");
  LoadURL(
      "https://example.com/"
      "test.html#:~:text=this%20is%20a%20test%20page");
  request.Complete(R"HTML(
    <!DOCTYPE html>
    <style>p { font: 10px/1 Ahem; }</style>
    <p id="first">This is a test page</p>
    <p id="two">Second test page two</p>
  )HTML");
  RunUntilTextFragmentFinalization();

  MockUnhandledTapNotifierImpl mock_notifier;
  GetDocument().GetFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
      mojom::blink::UnhandledTapNotifier::Name_,
      WTF::BindRepeating(&MockUnhandledTapNotifierImpl::Bind,
                         WTF::Unretained(&mock_notifier)));

  Range* range = Range::Create(GetDocument());
  range->setStart(GetDocument().getElementById(AtomicString("first")), 0,
                  IGNORE_EXCEPTION_FOR_TESTING);
  range->setEnd(GetDocument().getElementById(AtomicString("first")), 1,
                IGNORE_EXCEPTION_FOR_TESTING);
  ASSERT_EQ("This is a test page", range->GetText());

  mock_notifier.Reset();
  gfx::Point tap_point = range->BoundingBox().CenterPoint();
  SimulateTap(tap_point.x(), tap_point.y());

  base::RunLoop().RunUntilIdle();
  if (RuntimeEnabledFeatures::TextFragmentTapOpensContextMenuEnabled()) {
    EXPECT_FALSE(mock_notifier.WasUnhandledTap());
    EXPECT_FALSE(mock_notifier.ReceiverIsBound());
  } else {
    EXPECT_TRUE(mock_notifier.WasUnhandledTap());
    EXPECT_TRUE(mock_notifier.ReceiverIsBound());
  }

  range->setStart(GetDocument().getElementById(AtomicString("two")), 0,
                  IGNORE_EXCEPTION_FOR_TESTING);
  range->setEndAfter(GetDocument().getElementById(AtomicString("two")),
                     IGNORE_EXCEPTION_FOR_TESTING);
  ASSERT_EQ("Second test page two", range->GetText());

  mock_notifier.Reset();
  tap_point = range->BoundingBox().CenterPoint();
  SimulateTap(tap_point.x(), tap_point.y());

  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(mock_notifier.WasUnhandledTap());
  EXPECT_TRUE(mock_notifier.ReceiverIsBound());
}
#endif  // BUILDFLAG(ENABLE_UNHANDLED_TAP)

TEST_F(TextFragmentAnchorTest, TapOpeningContextMenuWithDirtyLifecycleNoCrash) {}

// Test for https://crbug.com/1453658. Trips a CHECK because an AnnotationAgent
// unexpectedly calls Attach a second time after initially succeeding because
// the matched range becomes collapsed.
TEST_F(TextFragmentAnchorTest, InitialMatchingIsCollapsedCrash) {}

// Test the behavior of removing matched text while waiting to expand a
// hidden=until-found section. We mostly care that this doesn't crash or
// violate any state CHECKs.
TEST_F(TextFragmentAnchorTest, InitialMatchPendingBecomesCollapsed) {}

// These tests are specifically testing the post-load timer task so use
// the real clock to faithfully reproduce real-world behavior.
class TextFragmentAnchorPostLoadTest : public TextFragmentAnchorTestController {};

// Ensure a content added shortly after load is found.
TEST_F(TextFragmentAnchorPostLoadTest, ContentAddedPostLoad) {}

// Ensure a content added shortly after load is found.
TEST_F(TextFragmentAnchorPostLoadTest, HiddenAfterFoundPostLoad) {}

// Ensure that the text fragment is searched within the delay time after load if
// DOM hasn't been mutated.
TEST_F(TextFragmentAnchorPostLoadTest, PostLoadSearchEndsWithoutDomMutation) {}

// Ensure that the post-load text fragment search is pushed back each time DOM
// is mutated.
TEST_F(TextFragmentAnchorPostLoadTest, PostLoadSearchTimesOut) {}

}  // namespace

}  // namespace blink