910e62b5创建于 1月15日历史提交
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/link.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/ui_base_switches.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/test/event_generator.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/base_control_test_widget.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/test/view_metadata_test_utils.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

class LinkTest : public test::BaseControlTestWidget {
 public:
  LinkTest() = default;
  LinkTest(const LinkTest&) = delete;
  LinkTest& operator=(const LinkTest&) = delete;
  ~LinkTest() override = default;

  void SetUp() override {
    test::BaseControlTestWidget::SetUp();

    event_generator_ = std::make_unique<ui::test::EventGenerator>(
        GetContext(), widget()->GetNativeWindow());
  }

  void TearDown() override {
    link_ = nullptr;
    test::BaseControlTestWidget::TearDown();
  }

 protected:
  void CreateWidgetContent(View* container) override {
    // Create a widget containing a link which does not take the full size.
    link_ = container->AddChildView(std::make_unique<Link>(u"TestLink"));
    link_->SetBoundsRect(
        gfx::ScaleToEnclosedRect(container->GetLocalBounds(), 0.5f));
  }

  Link* link() { return link_; }
  ui::test::EventGenerator* event_generator() { return event_generator_.get(); }

 public:
  raw_ptr<Link> link_ = nullptr;
  std::unique_ptr<ui::test::EventGenerator> event_generator_;
};

}  // namespace

TEST_F(LinkTest, Metadata) {
  link()->SetMultiLine(true);
  test::TestViewMetadata(link());
}

TEST_F(LinkTest, TestLinkClick) {
  bool link_clicked = false;
  link()->SetCallback(base::BindRepeating(
      [](bool* link_clicked) { *link_clicked = true; }, &link_clicked));
  link()->SizeToPreferredSize();
  gfx::Point point = link()->bounds().CenterPoint();
  ui::MouseEvent release(ui::EventType::kMouseReleased, point, point,
                         ui::EventTimeForNow(), ui::EF_LEFT_MOUSE_BUTTON,
                         ui::EF_LEFT_MOUSE_BUTTON);
  link()->OnMouseReleased(release);
  EXPECT_TRUE(link_clicked);
}

TEST_F(LinkTest, TestLinkTap) {
  bool link_clicked = false;
  link()->SetCallback(base::BindRepeating(
      [](bool* link_clicked) { *link_clicked = true; }, &link_clicked));
  link()->SizeToPreferredSize();
  gfx::Point point = link()->bounds().CenterPoint();
  ui::GestureEvent tap_event(
      point.x(), point.y(), 0, ui::EventTimeForNow(),
      ui::GestureEventDetails(ui::EventType::kGestureTap));
  link()->OnGestureEvent(&tap_event);
  EXPECT_TRUE(link_clicked);
}

// Tests that hovering and unhovering a link adds and removes an underline.
TEST_F(LinkTest, TestUnderlineOnHover) {
  // A link should be underlined.
  const gfx::Rect link_bounds = link()->GetBoundsInScreen();
  const gfx::Point off_link = link_bounds.bottom_right() + gfx::Vector2d(1, 1);
  event_generator()->MoveMouseTo(off_link);
  EXPECT_FALSE(link()->IsMouseHovered());
  const auto link_underlined = [link = link()]() {
    return !!(link->font_list().GetFontStyle() & gfx::Font::UNDERLINE);
  };
  EXPECT_TRUE(link_underlined());

  // A non-hovered link should should be underlined.
  // For a11y, A link should be underlined by default. If forcefuly remove an
  // underline, the underline appears according to hovering.
  link()->SetForceUnderline(false);
  EXPECT_FALSE(link_underlined());

  // Hovering the link should underline it.
  event_generator()->MoveMouseTo(link_bounds.CenterPoint());
  EXPECT_TRUE(link()->IsMouseHovered());
  EXPECT_TRUE(link_underlined());

  // Un-hovering the link should remove the underline again.
  event_generator()->MoveMouseTo(off_link);
  EXPECT_FALSE(link()->IsMouseHovered());
  EXPECT_FALSE(link_underlined());
}

// Tests that focusing and unfocusing a link keeps the underline and adds
// focus ring.
TEST_F(LinkTest, TestUnderlineAndFocusRingOnFocus) {
  const auto link_underlined = [link = link()]() {
    return !!(link->font_list().GetFontStyle() & gfx::Font::UNDERLINE);
  };

  // A non-focused link should be underlined and not have a focus ring.
  EXPECT_TRUE(link_underlined());
  EXPECT_FALSE(views::FocusRing::Get(link())->ShouldPaintForTesting());

  // A focused link should be underlined and it should have a focus ring.
  link()->RequestFocus();
  EXPECT_TRUE(link_underlined());
  EXPECT_TRUE(views::FocusRing::Get(link())->ShouldPaintForTesting());
}

TEST_F(LinkTest, AccessibleProperties) {
  ui::AXNodeData data;
  link()->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            u"TestLink");
  EXPECT_EQ(link()->GetViewAccessibility().GetCachedName(), u"TestLink");
  EXPECT_EQ(data.role, ax::mojom::Role::kLink);
  EXPECT_FALSE(link()->GetViewAccessibility().GetIsIgnored());

  // Setting the accessible name to a non-empty string should replace the name
  // from the link text.
  data = ui::AXNodeData();
  std::u16string accessible_name = u"Accessible Name";
  link()->GetViewAccessibility().SetName(accessible_name);
  link()->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            accessible_name);
  EXPECT_EQ(link()->GetViewAccessibility().GetCachedName(), accessible_name);
  EXPECT_EQ(data.role, ax::mojom::Role::kLink);
  EXPECT_FALSE(link()->GetViewAccessibility().GetIsIgnored());

  // Setting the accessible name to an empty string should cause the link text
  // to be used as the name.
  data = ui::AXNodeData();
  link()->GetViewAccessibility().SetName(std::u16string());
  link()->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            u"TestLink");
  EXPECT_EQ(link()->GetViewAccessibility().GetCachedName(), u"TestLink");
  EXPECT_EQ(data.role, ax::mojom::Role::kLink);
  EXPECT_FALSE(link()->GetViewAccessibility().GetIsIgnored());

  // Setting the link to an empty string without setting a new accessible
  // name should cause the view to become "ignored" again.
  data = ui::AXNodeData();
  link()->SetText(std::u16string());
  link()->GetViewAccessibility().GetAccessibleNodeData(&data);
  EXPECT_EQ(data.GetString16Attribute(ax::mojom::StringAttribute::kName),
            std::u16string());
  EXPECT_EQ(link()->GetViewAccessibility().GetCachedName(), std::u16string());
  EXPECT_EQ(data.role, ax::mojom::Role::kLink);
  EXPECT_TRUE(link()->GetViewAccessibility().GetIsIgnored());
}

}  // namespace views