chromium/ui/views/accessibility/view_ax_platform_node_delegate_win_unittest.cc

// Copyright 2013 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/accessibility/view_ax_platform_node_delegate_win.h"

#include <oleacc.h>
#include <wrl/client.h>

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

#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_variant.h"
#include "third_party/iaccessible2/ia2_api_all.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_constants.mojom.h"
#include "ui/accessibility/platform/ax_platform_node_win.h"
#include "ui/gfx/render_text_test_api.h"
#include "ui/views/accessibility/test_list_grid_view.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/controls/textfield/textfield_test_api.h"
#include "ui/views/test/views_test_base.h"

using base::win::ScopedBstr;
using base::win::ScopedVariant;
using Microsoft::WRL::ComPtr;

namespace views {
namespace test {

#define EXPECT_UIA_BOOL_EQ(node, property_id, expected)               \
  {                                                                   \
    ScopedVariant expectedVariant(expected);                          \
    ASSERT_EQ(VT_BOOL, expectedVariant.type());                       \
    ScopedVariant actual;                                             \
    ASSERT_HRESULT_SUCCEEDED(                                         \
        node->GetPropertyValue(property_id, actual.Receive()));       \
    EXPECT_EQ(expectedVariant.ptr()->boolVal, actual.ptr()->boolVal); \
  }

namespace {

// Whether |left| represents the same COM object as |right|.
template <typename T, typename U>
bool IsSameObject(T* left, U* right) {
  if (!left && !right)
    return true;

  if (!left || !right)
    return false;

  ComPtr<IUnknown> left_unknown;
  left->QueryInterface(IID_PPV_ARGS(&left_unknown));

  ComPtr<IUnknown> right_unknown;
  right->QueryInterface(IID_PPV_ARGS(&right_unknown));

  return left_unknown == right_unknown;
}

}  // namespace

class ViewAXPlatformNodeDelegateWinTest : public ViewsTestBase {
 public:
  ViewAXPlatformNodeDelegateWinTest() = default;
  ~ViewAXPlatformNodeDelegateWinTest() override = default;

 protected:
  void GetIAccessible2InterfaceForView(View* view, IAccessible2_2** result) {
    ComPtr<IAccessible> view_accessible(view->GetNativeViewAccessible());
    ComPtr<IServiceProvider> service_provider;
    ASSERT_EQ(S_OK, view_accessible.As(&service_provider));
    ASSERT_EQ(S_OK, service_provider->QueryService(IID_IAccessible2_2, result));
  }

  ComPtr<IRawElementProviderSimple> GetIRawElementProviderSimple(View* view) {
    ComPtr<IRawElementProviderSimple> result;
    EXPECT_HRESULT_SUCCEEDED(view->GetNativeViewAccessible()->QueryInterface(
        __uuidof(IRawElementProviderSimple), &result));
    return result;
  }

  ComPtr<IAccessible2> ToIAccessible2(ComPtr<IAccessible> accessible) {
    CHECK(accessible);
    ComPtr<IServiceProvider> service_provider;
    accessible.As(&service_provider);
    ComPtr<IAccessible2> result;
    CHECK(SUCCEEDED(service_provider->QueryService(IID_IAccessible2,
                                                   IID_PPV_ARGS(&result))));
    return result;
  }
};

TEST_F(ViewAXPlatformNodeDelegateWinTest, TextfieldAccessibility) {
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());

  Textfield* textfield = new Textfield;
  textfield->GetViewAccessibility().SetName(u"Name");
  textfield->SetText(u"Value");
  content->AddChildView(textfield);

  ComPtr<IAccessible> content_accessible(content->GetNativeViewAccessible());
  LONG child_count = 0;
  ASSERT_EQ(S_OK, content_accessible->get_accChildCount(&child_count));
  EXPECT_EQ(1, child_count);

  ComPtr<IDispatch> textfield_dispatch;
  ComPtr<IAccessible> textfield_accessible;
  ScopedVariant child_index(1);
  ASSERT_EQ(S_OK,
            content_accessible->get_accChild(child_index, &textfield_dispatch));
  ASSERT_EQ(S_OK, textfield_dispatch.As(&textfield_accessible));

  ASSERT_EQ(S_OK, textfield_accessible->get_accChildCount(&child_count));
  EXPECT_EQ(0, child_count)
      << "Text fields should be leaf nodes on this platform, otherwise no "
         "descendants will be recognized by assistive software.";

  ScopedBstr name;
  ScopedVariant childid_self(CHILDID_SELF);
  ASSERT_EQ(S_OK,
            textfield_accessible->get_accName(childid_self, name.Receive()));
  EXPECT_STREQ(L"Name", name.Get());

  ScopedBstr value;
  ASSERT_EQ(S_OK,
            textfield_accessible->get_accValue(childid_self, value.Receive()));
  EXPECT_STREQ(L"Value", value.Get());

  ScopedBstr new_value(L"New value");
  ASSERT_EQ(S_OK,
            textfield_accessible->put_accValue(childid_self, new_value.Get()));
  EXPECT_EQ(u"New value", textfield->GetText());
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, TextfieldAssociatedLabel) {
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());

  Label* label = new Label(u"Label");
  content->AddChildView(label);
  Textfield* textfield = new Textfield;
  textfield->GetViewAccessibility().SetName(*label);
  content->AddChildView(textfield);

  ComPtr<IAccessible> content_accessible(content->GetNativeViewAccessible());
  LONG child_count = 0;
  ASSERT_EQ(S_OK, content_accessible->get_accChildCount(&child_count));
  ASSERT_EQ(2L, child_count);

  ComPtr<IDispatch> textfield_dispatch;
  ComPtr<IAccessible> textfield_accessible;
  ScopedVariant child_index(2);
  ASSERT_EQ(S_OK,
            content_accessible->get_accChild(child_index, &textfield_dispatch));
  ASSERT_EQ(S_OK, textfield_dispatch.As(&textfield_accessible));

  ScopedBstr name;
  ScopedVariant childid_self(CHILDID_SELF);
  ASSERT_EQ(S_OK,
            textfield_accessible->get_accName(childid_self, name.Receive()));
  ASSERT_STREQ(L"Label", name.Get());

  ComPtr<IAccessible2_2> textfield_ia2;
  EXPECT_EQ(S_OK, textfield_accessible.As(&textfield_ia2));
  ScopedBstr type(IA2_RELATION_LABELLED_BY);
  IUnknown** targets;
  LONG n_targets;
  EXPECT_EQ(S_OK, textfield_ia2->get_relationTargetsOfType(
                      type.Get(), 0, &targets, &n_targets));
  ASSERT_EQ(1, n_targets);
  ComPtr<IUnknown> label_unknown(targets[0]);
  ComPtr<IAccessible> label_accessible;
  ASSERT_EQ(S_OK, label_unknown.As(&label_accessible));
  ScopedVariant role;
  EXPECT_EQ(S_OK, label_accessible->get_accRole(childid_self, role.Receive()));
  EXPECT_EQ(ROLE_SYSTEM_STATICTEXT, V_I4(role.ptr()));
}

// A subclass of ViewAXPlatformNodeDelegateWinTest that we run twice,
// first where we create an transient child widget (child = false), the second
// time where we create a child widget (child = true).
class ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag
    : public ViewAXPlatformNodeDelegateWinTest,
      public testing::WithParamInterface<bool> {
 public:
  ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag() = default;
  ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag(
      const ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag&) = delete;
  ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag& operator=(
      const ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag&) = delete;
  ~ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag() override = default;
};

INSTANTIATE_TEST_SUITE_P(All,
                         ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag,
                         testing::Bool());

TEST_P(ViewAXPlatformNodeDelegateWinTestWithBoolChildFlag, AuraChildWidgets) {
  // Create the parent widget.
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  init_params.bounds = gfx::Rect(0, 0, 400, 200);
  widget->Init(std::move(init_params));
  widget->Show();

  // Initially it has 1 child.
  ComPtr<IAccessible> root_view_accessible(
      widget->GetRootView()->GetNativeViewAccessible());
  LONG child_count = 0;
  ASSERT_EQ(S_OK, root_view_accessible->get_accChildCount(&child_count));
  ASSERT_EQ(1L, child_count);

  // Create the child widget, one of two ways (see below).
  auto child_widget = std::make_unique<Widget>();
  Widget::InitParams child_init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_BUBBLE);
  child_init_params.parent = widget->GetNativeView();
  child_init_params.bounds = gfx::Rect(30, 40, 100, 50);

  // NOTE: this test is run two times, GetParam() returns a different
  // value each time. The first time we test with child = false,
  // making this an owned widget (a transient child).  The second time
  // we test with child = true, making it a child widget->
  child_init_params.child = GetParam();

  child_widget->Init(std::move(child_init_params));
  child_widget->Show();

  // Now the IAccessible for the parent widget should have 2 children.
  ASSERT_EQ(S_OK, root_view_accessible->get_accChildCount(&child_count));
  ASSERT_EQ(2L, child_count);

  // Ensure the bounds of the parent widget are as expected.
  ScopedVariant childid_self(CHILDID_SELF);
  LONG x, y, width, height;
  ASSERT_EQ(S_OK, root_view_accessible->accLocation(&x, &y, &width, &height,
                                                    childid_self));
  EXPECT_EQ(0, x);
  EXPECT_EQ(0, y);
  EXPECT_EQ(400, width);
  EXPECT_EQ(200, height);

  // Get the IAccessible for the second child of the parent widget,
  // which should be the one for our child widget->
  ComPtr<IDispatch> child_widget_dispatch;
  ComPtr<IAccessible> child_widget_accessible;
  ScopedVariant child_index_2(2);
  ASSERT_EQ(S_OK, root_view_accessible->get_accChild(child_index_2,
                                                     &child_widget_dispatch));
  ASSERT_EQ(S_OK, child_widget_dispatch.As(&child_widget_accessible));

  // Check the bounds of the IAccessible for the child widget->
  // This is a sanity check to make sure we have the right object
  // and not some other view.
  ASSERT_EQ(S_OK, child_widget_accessible->accLocation(&x, &y, &width, &height,
                                                       childid_self));
  EXPECT_EQ(30, x);
  EXPECT_EQ(40, y);
  EXPECT_EQ(100, width);
  EXPECT_EQ(50, height);

  // Now make sure that querying the parent of the child gets us back to
  // the original parent.
  ComPtr<IDispatch> child_widget_parent_dispatch;
  ComPtr<IAccessible> child_widget_parent_accessible;
  ASSERT_EQ(S_OK, child_widget_accessible->get_accParent(
                      &child_widget_parent_dispatch));
  ASSERT_EQ(S_OK,
            child_widget_parent_dispatch.As(&child_widget_parent_accessible));
  EXPECT_EQ(root_view_accessible.Get(), child_widget_parent_accessible.Get());
}

// Flaky on Windows: https://crbug.com/461837.
TEST_F(ViewAXPlatformNodeDelegateWinTest, DISABLED_RetrieveAllAlerts) {
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());

  View* infobar = new View;
  content->AddChildView(infobar);

  View* infobar2 = new View;
  content->AddChildView(infobar2);

  View* root_view = content->parent();
  ASSERT_EQ(nullptr, root_view->parent());

  ComPtr<IAccessible2_2> root_view_accessible;
  GetIAccessible2InterfaceForView(root_view, &root_view_accessible);

  ComPtr<IAccessible2_2> infobar_accessible;
  GetIAccessible2InterfaceForView(infobar, &infobar_accessible);

  ComPtr<IAccessible2_2> infobar2_accessible;
  GetIAccessible2InterfaceForView(infobar2, &infobar2_accessible);

  // Initially, there are no alerts
  ScopedBstr alerts_bstr(L"alerts");
  IUnknown** targets;
  LONG n_targets;
  ASSERT_EQ(S_FALSE, root_view_accessible->get_relationTargetsOfType(
                         alerts_bstr.Get(), 0, &targets, &n_targets));
  ASSERT_EQ(0, n_targets);

  // Fire alert events on the infobars.
  infobar->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
  infobar2->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);

  // Now calling get_relationTargetsOfType should retrieve the alerts.
  ASSERT_EQ(S_OK, root_view_accessible->get_relationTargetsOfType(
                      alerts_bstr.Get(), 0, &targets, &n_targets));
  ASSERT_EQ(2, n_targets);
  {
    // SAFETY: get_relationTargetsOfType() is a COM interface which guarantees
    // that exactly n_targets pointers are available starting at targets.
    UNSAFE_BUFFERS(base::span<IUnknown*> targets_span(
                       targets, base::checked_cast<size_t>(n_targets));)
    ASSERT_TRUE(IsSameObject(infobar_accessible.Get(), targets_span[0]));
    ASSERT_TRUE(IsSameObject(infobar2_accessible.Get(), targets_span[1]));
  }
  CoTaskMemFree(targets);

  // If we set max_targets to 1, we should only get the first one.
  ASSERT_EQ(S_OK, root_view_accessible->get_relationTargetsOfType(
                      alerts_bstr.Get(), 1, &targets, &n_targets));
  ASSERT_EQ(1, n_targets);
  ASSERT_TRUE(IsSameObject(infobar_accessible.Get(), targets[0]));
  CoTaskMemFree(targets);

  // If we delete the first view, we should only get the second one now.
  delete infobar;
  ASSERT_EQ(S_OK, root_view_accessible->get_relationTargetsOfType(
                      alerts_bstr.Get(), 0, &targets, &n_targets));
  ASSERT_EQ(1, n_targets);
  ASSERT_TRUE(IsSameObject(infobar2_accessible.Get(), targets[0]));
  CoTaskMemFree(targets);
}

// Test trying to retrieve child widgets during window close does not crash.
TEST_F(ViewAXPlatformNodeDelegateWinTest, GetAllOwnedWidgetsCrash) {
  Widget widget;
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  widget.Init(std::move(init_params));
  widget.CloseNow();

  LONG child_count = 0;
  ComPtr<IAccessible> content_accessible(
      widget.GetRootView()->GetNativeViewAccessible());
  EXPECT_EQ(S_OK, content_accessible->get_accChildCount(&child_count));
  EXPECT_EQ(1L, child_count);
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, WindowHasRoleApplication) {
  // We expect that our internal window object does not expose
  // ROLE_SYSTEM_WINDOW, but ROLE_SYSTEM_PANE instead.
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_WINDOW);
  widget->Init(std::move(init_params));

  ComPtr<IAccessible> accessible(
      widget->GetRootView()->GetNativeViewAccessible());
  ScopedVariant childid_self(CHILDID_SELF);
  ScopedVariant role;
  EXPECT_EQ(S_OK, accessible->get_accRole(childid_self, role.Receive()));
  EXPECT_EQ(VT_I4, role.type());
  EXPECT_EQ(ROLE_SYSTEM_PANE, V_I4(role.ptr()));
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, Overrides) {
  // We expect that our internal window object does not expose
  // ROLE_SYSTEM_WINDOW, but ROLE_SYSTEM_PANE instead.
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* contents_view = widget->SetContentsView(std::make_unique<View>());

  View* alert_view = new ScrollView;
  alert_view->GetViewAccessibility().SetRole(ax::mojom::Role::kAlert);
  alert_view->GetViewAccessibility().SetName(u"Name",
                                             ax::mojom::NameFrom::kAttribute);
  alert_view->GetViewAccessibility().SetDescription("Description");
  alert_view->GetViewAccessibility().SetIsLeaf(true);
  contents_view->AddChildView(alert_view);

  // Descendant should be ignored because the parent uses SetIsLeaf().
  View* ignored_descendant = new View;
  alert_view->AddChildView(ignored_descendant);

  ComPtr<IAccessible> content_accessible(
      contents_view->GetNativeViewAccessible());
  ScopedVariant child_index(1);

  // Role.
  ScopedVariant role;
  EXPECT_EQ(S_OK, content_accessible->get_accRole(child_index, role.Receive()));
  EXPECT_EQ(VT_I4, role.type());
  EXPECT_EQ(ROLE_SYSTEM_ALERT, V_I4(role.ptr()));

  // Name.
  ScopedBstr name;
  ASSERT_EQ(S_OK, content_accessible->get_accName(child_index, name.Receive()));
  ASSERT_STREQ(L"Name", name.Get());

  // Description.
  ScopedBstr description;
  ASSERT_EQ(S_OK, content_accessible->get_accDescription(
                      child_index, description.Receive()));
  ASSERT_STREQ(L"Description", description.Get());

  // Get the child accessible.
  ComPtr<IDispatch> alert_dispatch;
  ComPtr<IAccessible> alert_accessible;
  ASSERT_EQ(S_OK,
            content_accessible->get_accChild(child_index, &alert_dispatch));
  ASSERT_EQ(S_OK, alert_dispatch.As(&alert_accessible));

  // Child accessible is a leaf.
  LONG child_count = 0;
  ASSERT_EQ(S_OK, alert_accessible->get_accChildCount(&child_count));
  ASSERT_EQ(0, child_count);

  ComPtr<IDispatch> child_dispatch;
  ASSERT_EQ(E_INVALIDARG,
            alert_accessible->get_accChild(child_index, &child_dispatch));
  ASSERT_EQ(child_dispatch.Get(), nullptr);
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, GridRowColumnCount) {
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());
  TestListGridView* grid = new TestListGridView();
  content->AddChildView(grid);

  Microsoft::WRL::ComPtr<IGridProvider> grid_provider;
  EXPECT_HRESULT_SUCCEEDED(
      grid->GetViewAccessibility().GetNativeObject()->QueryInterface(
          __uuidof(IGridProvider), &grid_provider));

  // If set, aria row/column count takes precedence over table row/column count.
  // Expect E_UNEXPECTED if the result is kUnknownAriaColumnOrRowCount (-1) or
  // if neither is set.
  int row_count;
  int column_count;

  // aria row/column count = not set
  // table row/column count = not set
  grid->UnsetAriaTableSize();
  grid->UnsetTableSize();
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_RowCount(&row_count));
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_ColumnCount(&column_count));
  EXPECT_EQ(0, row_count);
  EXPECT_EQ(0, column_count);
  // To do still: When nothing is set, currently
  // AXPlatformNodeDelegateBase::GetTable{Row/Col}Count() returns 0 Should it
  // return std::nullopt if the attribute is not set? Like
  // GetTableAria{Row/Col}Count()
  // EXPECT_EQ(E_UNEXPECTED, grid_provider->get_RowCount(&row_count));

  // aria row/column count = 2
  // table row/column count = not set
  grid->SetAriaTableSize(2, 2);
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_RowCount(&row_count));
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_ColumnCount(&column_count));
  EXPECT_EQ(2, row_count);
  EXPECT_EQ(2, column_count);

  // aria row/column count = kUnknownAriaColumnOrRowCount
  // table row/column count = not set
  grid->SetAriaTableSize(ax::mojom::kUnknownAriaColumnOrRowCount,
                         ax::mojom::kUnknownAriaColumnOrRowCount);
  EXPECT_EQ(E_UNEXPECTED, grid_provider->get_RowCount(&row_count));
  EXPECT_EQ(E_UNEXPECTED, grid_provider->get_ColumnCount(&column_count));

  // aria row/column count = 3
  // table row/column count = 4
  grid->SetAriaTableSize(3, 3);
  grid->SetTableSize(4, 4);
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_RowCount(&row_count));
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_ColumnCount(&column_count));
  EXPECT_EQ(3, row_count);
  EXPECT_EQ(3, column_count);

  // aria row/column count = not set
  // table row/column count = 4
  grid->UnsetAriaTableSize();
  grid->SetTableSize(4, 4);
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_RowCount(&row_count));
  EXPECT_HRESULT_SUCCEEDED(grid_provider->get_ColumnCount(&column_count));
  EXPECT_EQ(4, row_count);
  EXPECT_EQ(4, column_count);

  // aria row/column count = not set
  // table row/column count = kUnknownAriaColumnOrRowCount
  grid->SetTableSize(ax::mojom::kUnknownAriaColumnOrRowCount,
                     ax::mojom::kUnknownAriaColumnOrRowCount);
  EXPECT_EQ(E_UNEXPECTED, grid_provider->get_RowCount(&row_count));
  EXPECT_EQ(E_UNEXPECTED, grid_provider->get_ColumnCount(&column_count));
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, IsUIAControlIsTrueEvenWhenReadonly) {
  // This test ensures that the value returned by
  // AXPlatformNodeWin::IsUIAControl returns true even if the element is
  // read-only. The previous implementation was incorrect and used to return
  // false for read-only views, causing all sorts of issues with ATs.
  //
  // Since we can't test IsUIAControl directly, we go through the
  // UIA_IsControlElementPropertyId, which is computed using IsUIAControl.

  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());

  Textfield* text_field = new Textfield();
  text_field->SetReadOnly(true);
  content->AddChildView(text_field);

  ComPtr<IRawElementProviderSimple> textfield_provider =
      GetIRawElementProviderSimple(text_field);
  EXPECT_UIA_BOOL_EQ(textfield_provider, UIA_IsControlElementPropertyId, true);
}

TEST_F(ViewAXPlatformNodeDelegateWinTest, TextPositionAt) {
  auto widget = std::make_unique<Widget>();
  Widget::InitParams init_params = CreateParams(
      Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
  widget->Init(std::move(init_params));

  View* content = widget->SetContentsView(std::make_unique<View>());

  Label* label = new Label(u"Label's Name");
  content->AddChildView(label);
  label->GetViewAccessibility().EnsureAtomicViewAXTreeManager();
  ViewAXPlatformNodeDelegate* label_accessibility =
      static_cast<ViewAXPlatformNodeDelegate*>(&label->GetViewAccessibility());
  label_accessibility->GetData();

  ui::AXNodePosition::AXPositionInstance actual_position =
      label_accessibility->CreateTextPositionAt(
          0, ax::mojom::TextAffinity::kDownstream);
  EXPECT_NE(nullptr, actual_position.get());
  EXPECT_EQ(0, actual_position->text_offset());
  EXPECT_EQ(u"Label's Name", actual_position->GetText());
}

//
// TableView tests.
//

namespace {
class TestTableModel : public ui::TableModel {
 public:
  TestTableModel() = default;

  TestTableModel(const TestTableModel&) = delete;
  TestTableModel& operator=(const TestTableModel&) = delete;

  // ui::TableModel:
  size_t RowCount() override { return 3; }

  std::u16string GetText(size_t row, int column_id) override {
    constexpr std::array<std::array<const char* const, 5>, 3> cells = {{
        {{"Australia", "24,584,620", "1,323,421,072,479"}},
        {{"Spain", "46,647,428", "1,314,314,164,402"}},
        {{"Nigeria", "190.873,244", "375,745,486,521"}},
    }};

    return base::ASCIIToUTF16(cells[row % 5][column_id]);
  }

  void SetObserver(ui::TableModelObserver* observer) override {}
};
}  // namespace

class ViewAXPlatformNodeDelegateWinTableTest
    : public ViewAXPlatformNodeDelegateWinTest {
  void SetUp() override {
    ViewAXPlatformNodeDelegateWinTest::SetUp();

    std::vector<ui::TableColumn> columns;
    columns.push_back(TestTableColumn(0, "Country"));
    columns.push_back(TestTableColumn(1, "Population"));
    columns.push_back(TestTableColumn(2, "GDP"));

    model_ = std::make_unique<TestTableModel>();
    auto table = std::make_unique<TableView>(model_.get(), columns,
                                             TableType::kTextOnly, true);
    table_ = table.get();

    widget_ = std::make_unique<Widget>();
    Widget::InitParams init_params = CreateParams(
        Widget::InitParams::CLIENT_OWNS_WIDGET, Widget::InitParams::TYPE_POPUP);
    init_params.bounds = gfx::Rect(0, 0, 400, 400);
    widget_->Init(std::move(init_params));

    View* content = widget_->SetContentsView(std::make_unique<View>());
    content->AddChildView(
        TableView::CreateScrollViewWithTable(std::move(table)));
    widget_->Show();
  }

  void TearDown() override {
    if (!widget_->IsClosed())
      widget_->Close();
    ViewAXPlatformNodeDelegateWinTest::TearDown();
  }

  ui::TableColumn TestTableColumn(int id, const std::string& title) {
    ui::TableColumn column;
    column.id = id;
    column.title = base::ASCIIToUTF16(title.c_str());
    column.sortable = true;
    return column;
  }

 protected:
  std::unique_ptr<TestTableModel> model_;
  std::unique_ptr<Widget> widget_;
  raw_ptr<TableView> table_ = nullptr;  // Owned by parent.
};

TEST_F(ViewAXPlatformNodeDelegateWinTableTest, TableCellAttributes) {
  ComPtr<IAccessible2_2> table_accessible;
  GetIAccessible2InterfaceForView(table_, &table_accessible);

  auto get_attributes = [&](int row_child, int cell_child) -> std::wstring {
    ComPtr<IDispatch> row_dispatch;
    CHECK_EQ(S_OK, table_accessible->get_accChild(ScopedVariant(row_child),
                                                  &row_dispatch));
    ComPtr<IAccessible> row;
    CHECK_EQ(S_OK, row_dispatch.As(&row));
    ComPtr<IAccessible2> ia2_row = ToIAccessible2(row);

    ComPtr<IDispatch> cell_dispatch;
    CHECK_EQ(S_OK,
             row->get_accChild(ScopedVariant(cell_child), &cell_dispatch));
    ComPtr<IAccessible> cell;
    CHECK_EQ(S_OK, cell_dispatch.As(&cell));
    ComPtr<IAccessible2> ia2_cell = ToIAccessible2(cell);

    ScopedBstr attributes_bstr;
    CHECK_EQ(S_OK, ia2_cell->get_attributes(attributes_bstr.Receive()));
    std::wstring attributes(attributes_bstr.Get());
    return attributes;
  };

  // These strings should NOT contain rowindex or colindex, since those
  // imply an ARIA override.
  EXPECT_EQ(
      get_attributes(1, 1),
      L"name-from:attribute;explicit-name:true;sort:none;class:AXVirtualView;");
  EXPECT_EQ(
      get_attributes(1, 2),
      L"name-from:attribute;explicit-name:true;sort:none;class:AXVirtualView;");
  EXPECT_EQ(get_attributes(2, 1),
            L"hidden:true;name-from:attribute;explicit-name:true;class:"
            L"AXVirtualView;");
  EXPECT_EQ(get_attributes(2, 2),
            L"hidden:true;name-from:attribute;explicit-name:true;class:"
            L"AXVirtualView;");
}

}  // namespace test

// Needs to be in the views namespace.
class ViewAXPlatformNodeDelegateWinInnerTextRangeTest
    : public test::ViewAXPlatformNodeDelegateWinTest {
 public:
  void SetUp() override {
    ViewAXPlatformNodeDelegateWinTest::SetUp();

    scoped_feature_list_.InitAndEnableFeature(features::kUiaProvider);

    widget_ = std::make_unique<Widget>();

    Widget::InitParams params =
        CreateParams(Widget::InitParams::CLIENT_OWNS_WIDGET,
                     Widget::InitParams::TYPE_WINDOW);
    params.bounds = gfx::Rect(0, 0, 200, 200);
    widget_->Init(std::move(params));

    textfield_ = new Textfield();
    textfield_->SetBounds(0, 0, 100, 40);
    widget_->GetContentsView()->AddChildView(textfield_.get());

    TextfieldTestApi textfield_test_api(textfield_);
    textfield_test_api.GetRenderText()->set_glyph_width_for_test(5);
    textfield_test_api.GetRenderText()->set_glyph_height_for_test(8);

    label_ = new Label();
    widget_->GetContentsView()->AddChildView(label_.get());

    // TODO(crbug.com/40924888): This is not obvious, but the
    // AtomicViewAXTreeManager gets initialized from this GetData() call. This
    // won't be needed anymore once we finish the ViewsAX project and remove the
    // temporary solution.
    textfield_delegate()->GetData();
    CHECK(textfield_delegate()->GetAtomicViewAXTreeManagerForTesting());

    label_delegate()->GetData();
    CHECK(label_delegate()->GetAtomicViewAXTreeManagerForTesting());
  }

  void TearDown() override {
    textfield_ = nullptr;
    label_ = nullptr;
    if (!widget_->IsClosed()) {
      widget_->Close();
    }
    ViewsTestBase::TearDown();
  }

  ViewAXPlatformNodeDelegate* textfield_delegate() {
    return static_cast<ViewAXPlatformNodeDelegate*>(
        &textfield_->GetViewAccessibility());
  }
  ViewAXPlatformNodeDelegate* label_delegate() {
    return static_cast<ViewAXPlatformNodeDelegate*>(
        &label_->GetViewAccessibility());
  }

 protected:
  raw_ptr<Textfield> textfield_ = nullptr;  // Owned by views hierarchy.
  raw_ptr<Label> label_ = nullptr;          // Owned by views hierarchy.
  std::unique_ptr<Widget> widget_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest,
       EmptyTextfield_NonEmptyRect) {
  ui::AXOffscreenResult offscreen_result;

  gfx::Rect textfield_bounds = gfx::Rect(0, 0, 50, 100);
  textfield_->SetBoundsRect(textfield_bounds);
  gfx::Insets insets = textfield_->GetInsets();

  // An empty text field should expose bounds with a fixed width of 1.
  gfx::Rect bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      0, 1, ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(insets.left(), insets.top(), 1,
                textfield_bounds.height() - insets.top() - insets.bottom()),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest, EmptyLabel_EmptyRect) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  // An empty text field should expose bounds with a fixed width of 1.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      0, 1, ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(0, 0, 0, 0), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest, Textfield_LTR) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  // This string contains a glyph formed of 3 codepoints (the middle up, the
  // thumbs up emoji). This test validates that we expose the bounds of
  // individual glyphs, not codepoints.
  const char16_t kText[] = u"a\U0001F44D\uFE0Fb";
  constexpr gfx::Range kRange1 = gfx::Range(0, 1);  // Range of character 'a'.
  constexpr gfx::Range kRange2 =
      gfx::Range(1, 2);  // Range of the middle glyph.
  constexpr gfx::Range kRange3 = gfx::Range(2, 3);  // Range of character 'b'.
  constexpr gfx::Range kRange4 = gfx::Range(0, 3);  // Range of the entire text.

  constexpr int kGlyphWidth = 5;
  gfx::Rect textfield_bounds = gfx::Rect(0, 0, 10 * kGlyphWidth, 100);
  textfield_->SetBoundsRect(textfield_bounds);
  gfx::Insets insets = textfield_->GetInsets();

  textfield_->SetText(kText);
  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  textfield_delegate()->GetData();

  int height = textfield_bounds.height() - insets.top() - insets.bottom();
  int initial_x = 2 * insets.left();

  // Range 1: 'a'.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange1.start(), kRange1.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x, insets.top(), kGlyphWidth, height), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 2: middle glyph.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(initial_x + kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 3: 'b'.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange3.start(), kRange3.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(initial_x + 2 * kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 4: all text.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange4.start(), kRange4.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x, insets.top(), 3 * kGlyphWidth, height),
            bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest,
       Textfield_TextOverflow) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  // This string contains text that is too long to fit in the textfield.
  const char16_t kText[] = u"3.1415926535897932384626433832795";

  constexpr int kGlyphWidth = 5;
  // The textfield is 5 glyphs wide, so the text will overflow.
  gfx::Rect textfield_bounds = gfx::Rect(0, 0, 5 * kGlyphWidth, 100);
  textfield_->SetBoundsRect(textfield_bounds);
  gfx::Insets insets = textfield_->GetInsets();

  textfield_->SetText(kText);
  ui::AXNodeID id = textfield_delegate()->GetData().id;

  // Initialize the textfield's scroll offset to 0.
  ui::AXActionData set_selection_action_data_1;
  set_selection_action_data_1.action = ax::mojom::Action::kSetSelection;
  set_selection_action_data_1.anchor_node_id = id;
  set_selection_action_data_1.focus_node_id = id;
  set_selection_action_data_1.focus_offset = 0;
  set_selection_action_data_1.anchor_offset = 0;
  textfield_delegate()->AccessibilityPerformAction(set_selection_action_data_1);
  EXPECT_EQ(textfield_delegate()->GetData().GetIntAttribute(
                ax::mojom::IntAttribute::kScrollX),
            0);

  int height = textfield_bounds.height() - insets.top() - insets.bottom();
  int initial_x = 2 * insets.left();

  // 1. Check the bounds of the first 5 characters. They are on screen.
  constexpr gfx::Range kRange1 = gfx::Range(0, 5);
  // The expected width is as follows because we clip bounds to the container.
  int expected_width = textfield_bounds.width() - insets.left();
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange1.start(), kRange1.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x, insets.top(), expected_width, height), bounds);

  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // 2. Check the bounds of the last character. It's offscreen, because the
  // scroll offset is still 0 for now.
  constexpr size_t text_length = std::size(kText) - 1;
  constexpr gfx::Range kRange2 = gfx::Range(text_length - 1, text_length);
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(insets.left(), insets.top(), 0, 0), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOffscreen);

  // 3. Set the selection to the last character. This will scroll the textfield
  // and it should be onscreen now.
  // Perform the scroll.
  ui::AXActionData set_selection_action_data_2;
  set_selection_action_data_2.action = ax::mojom::Action::kSetSelection;
  set_selection_action_data_2.anchor_node_id = id;
  set_selection_action_data_2.focus_node_id = id;
  set_selection_action_data_2.focus_offset = kRange2.start();
  set_selection_action_data_2.anchor_offset = kRange2.end();
  textfield_delegate()->AccessibilityPerformAction(set_selection_action_data_2);
  int scroll_x = textfield_delegate()->GetData().GetIntAttribute(
      ax::mojom::IntAttribute::kScrollX);
  EXPECT_LT(scroll_x, 0);

  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  textfield_delegate()->GetData();

  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x + kRange2.start() * kGlyphWidth + scroll_x,
                      insets.top(), kGlyphWidth, height),
            bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest, Label_LTR) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  constexpr int kGlyphWidth = 5;
  constexpr int kGlyphHeight = 8;
  const char16_t kText[] = u"a\U0001F44D\uFE0Fb";
  constexpr gfx::Range kRange1 = gfx::Range(0, 1);  // Range of character 'a'.
  constexpr gfx::Range kRange2 = gfx::Range(1, 2);  // Range of the emoji.
  constexpr gfx::Range kRange3 = gfx::Range(2, 3);  // Range of character 'b'.
  constexpr gfx::Range kRange4 = gfx::Range(0, 3);  // Range of the entire text.

  label_->SetText(kText);
  label_->SetBoundsRect(gfx::Rect(0, 0, 10 * kGlyphWidth, 100));
  label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);

  // Since the glyph size can vary from one machine to another, we force set a
  // fixed glyph size to make the test deterministic and not flaky.
  label_->MaybeBuildDisplayText();
  gfx::test::RenderTextTestApi render_text_test_api(
      label_->display_text_.get());
  render_text_test_api.SetGlyphWidth(kGlyphWidth);
  render_text_test_api.SetGlyphHeight(kGlyphHeight);

  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  label_delegate()->GetData();

  // Range 1: 'a'.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange1.start(), kRange1.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(0, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 2: middle glyph.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(5, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 3: 'b'.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange3.start(), kRange3.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(10, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 4: all text.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange4.start(), kRange4.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(0, 0, 15, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest, Textfield_RTL) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  const char16_t kText[] = u"اللغة ";
  constexpr gfx::Range kRange1 = gfx::Range(0, 1);
  constexpr gfx::Range kRange2 = gfx::Range(1, 2);
  constexpr gfx::Range kRange3 = gfx::Range(2, 3);
  constexpr gfx::Range kRange4 = gfx::Range(3, 4);
  constexpr gfx::Range kRange5 = gfx::Range(4, 5);
  constexpr gfx::Range kRange6 = gfx::Range(0, 5);

  base::i18n::SetRTLForTesting(true);

  constexpr int kGlyphWidth = 5;
  gfx::Rect textfield_bounds = gfx::Rect(0, 0, 15 * kGlyphWidth, 100);
  textfield_->SetBoundsRect(textfield_bounds);
  gfx::Insets insets = textfield_->GetInsets();
  textfield_->SetHorizontalAlignment(gfx::ALIGN_RIGHT);

  textfield_->SetText(kText);
  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  textfield_delegate()->GetData();

  int height = textfield_bounds.height() - insets.top() - insets.bottom();
  // TODO(accessibility): The initial x offset should be the result of an
  // operation that takes into account the left insets, but because of
  // https://crbug.com/1508209, it happens to be consistently 11 on all try
  // bots. If this test starts failings, please reach out to
  // [email protected].
  //
  // 11 comes from 10 for the horizontal insets + 1 for the cursor width.
  int initial_x = widget_->GetWindowBoundsInScreen().width() - 11;

  // Range 1.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange1.start(), kRange1.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x, insets.top(), kGlyphWidth, height), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 2.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(initial_x - kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 3.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange3.start(), kRange3.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(initial_x - 2 * kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 4.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange4.start(), kRange4.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(initial_x - 3 * kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 5.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange5.start(), kRange5.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  // TODO(accessibility): The x offset here should be smaller than the one of
  // the previous glyph, but it's not. Investigate.
  EXPECT_EQ(
      gfx::Rect(initial_x - 4 * kGlyphWidth, insets.top(), kGlyphWidth, height),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 6.
  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      kRange6.start(), kRange6.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(initial_x - 4 * kGlyphWidth, insets.top(),
                      5 * kGlyphWidth, height),
            bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest, Label_RTL) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  constexpr int kGlyphWidth = 5;
  constexpr int kGlyphHeight = 8;
  const char16_t kText[] = u"اللغة ";
  constexpr gfx::Range kRange1 = gfx::Range(0, 1);
  constexpr gfx::Range kRange2 = gfx::Range(1, 2);
  constexpr gfx::Range kRange3 = gfx::Range(2, 3);
  constexpr gfx::Range kRange4 = gfx::Range(3, 4);
  constexpr gfx::Range kRange5 = gfx::Range(4, 5);
  constexpr gfx::Range kRange6 = gfx::Range(0, 5);

  base::i18n::SetRTLForTesting(true);

  label_->SetText(kText);
  label_->SetBoundsRect(gfx::Rect(0, 0, 10 * kGlyphWidth, 100));
  label_->SetHorizontalAlignment(gfx::ALIGN_RIGHT);

  // Since the glyph size can vary from one machine to another, we force set a
  // fixed glyph size to make the test deterministic and not flaky.
  label_->MaybeBuildDisplayText();
  gfx::test::RenderTextTestApi render_text_test_api(
      label_->display_text_.get());
  render_text_test_api.SetGlyphWidth(kGlyphWidth);
  render_text_test_api.SetGlyphHeight(kGlyphHeight);

  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  label_delegate()->GetData();

  // Range 1.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange1.start(), kRange1.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(170, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 2.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange2.start(), kRange2.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(165, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 3.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange3.start(), kRange3.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(160, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 4.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange4.start(), kRange4.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(155, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 5.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange5.start(), kRange5.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  // TODO(accessibility): The x offset here should be greater than the one of
  // the previous range, the previous glyph, but it's not. Investigate.
  EXPECT_EQ(gfx::Rect(150, 0, 5, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);

  // Range 6.
  bounds = label_delegate()->GetInnerTextRangeBoundsRect(
      kRange6.start(), kRange6.end(), ui::AXCoordinateSystem::kScreenDIPs,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(gfx::Rect(150, 0, 25, 100), bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest,
       Textfield_CreatePositionAt) {
  const std::u16string kText = u"text";
  textfield_->SetText(kText);

  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  ui::AXNodeData data = textfield_delegate()->GetData();

  ui::AXNodeID expected_node_id = data.id;
  ui::AXNodePosition::AXPositionInstance position;

  // 1. Validate that we can create a position at the beginning of the text.
  position = textfield_delegate()->CreatePositionAt(
      0, ax::mojom::TextAffinity::kDownstream);
  EXPECT_EQ(0, position->text_offset());
  EXPECT_EQ(expected_node_id, position->anchor_id());
  EXPECT_TRUE(position->IsTextPosition());

  // 2. Validate that we can create a position at the end of the text.
  position = textfield_delegate()->CreatePositionAt(
      kText.length(), ax::mojom::TextAffinity::kDownstream);
  EXPECT_EQ(kText.length(), static_cast<size_t>(position->text_offset()));
  EXPECT_EQ(expected_node_id, position->anchor_id());
  EXPECT_TRUE(position->IsTextPosition());

  // TODO(accessibility): Uncomment once https://crbug.com/1404289 is fixed.
  // // 3. Validate that we can't create a position at an invalid offset.
  // position = textfield_delegate()->CreatePositionAt(kText.length() + 1,
  // ax::mojom::TextAffinity::kDownstream); LOG(INFO) << position->ToString();
  // EXPECT_TRUE(position->IsNullPosition());

  // // 4. Clear the text and validate that we can't create a position.
  // position = textfield_delegate()->CreatePositionAt(0,
  // ax::mojom::TextAffinity::kDownstream);
  // EXPECT_TRUE(position->IsNullPosition());
}

TEST_F(ViewAXPlatformNodeDelegateWinInnerTextRangeTest,
       Textfield_ScreenPhysicalPixels) {
  ui::AXOffscreenResult offscreen_result;
  gfx::Rect bounds;

  base::i18n::SetRTLForTesting(false);

  constexpr int kGlyphWidth = 5;

  gfx::RenderText* render_text = TextfieldTestApi(textfield_).GetRenderText();
  render_text->set_glyph_width_for_test(5);
  gfx::Rect textfield_bounds = gfx::Rect(0, 0, 10 * kGlyphWidth, 100);
  textfield_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  textfield_->SetBoundsRect(textfield_bounds);
  gfx::Insets insets = textfield_->GetInsets();
  const char16_t kText[] = u"a";

  textfield_->SetText(kText);

  // TODO(crbug.com/40924888): This is not obvious, but we need to call
  // `GetData` to refresh the text offsets and accessible name. This won't be
  // needed anymore once we finish the ViewsAX project and remove the temporary
  // solution.
  textfield_delegate()->GetData();

  bounds = textfield_delegate()->GetInnerTextRangeBoundsRect(
      0, 1, ui::AXCoordinateSystem::kScreenPhysicalPixels,
      ui::AXClippingBehavior::kClipped, &offscreen_result);
  EXPECT_EQ(
      gfx::Rect(2 * insets.left(), 0 + insets.top(), kGlyphWidth,
                textfield_bounds.height() - insets.top() - insets.bottom()),
      bounds);
  EXPECT_EQ(offscreen_result, ui::AXOffscreenResult::kOnscreen);
}

}  // namespace views