blob: b5a87aa4e38587924754c7c8e4a8ab8ff3452c4a [file] [log] [blame]
// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <atk/atk.h>
#include <string>
#include <vector>
#include "base/functional/callback_helpers.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/version.h"
#include "build/build_config.h"
#include "content/browser/accessibility/accessibility_browsertest.h"
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "ui/accessibility/platform/ax_platform_node_auralinux.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/base/glib/scoped_gsignal.h"
// TODO(crbug.com/40248581): Remove this again.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
namespace content {
namespace {
const char16_t kBullet[2] = {u'\x2022', ' '};
const std::u16string kString16Bullet = std::u16string(kBullet, 2);
AtkObject* FindAtkObjectParentFrame(AtkObject* atk_object) {
while (atk_object) {
if (atk_object_get_role(atk_object) == ATK_ROLE_FRAME) {
return atk_object;
}
atk_object = atk_object_get_parent(atk_object);
}
return nullptr;
}
static bool IsAtkObjectFocused(AtkObject* object) {
AtkStateSet* state_set = atk_object_ref_state_set(object);
bool result = atk_state_set_contains_state(state_set, ATK_STATE_FOCUSED);
g_object_unref(state_set);
return result;
}
static bool IsAtkObjectEditable(AtkObject* object) {
AtkStateSet* state_set = atk_object_ref_state_set(object);
bool result = atk_state_set_contains_state(state_set, ATK_STATE_EDITABLE);
g_object_unref(state_set);
return result;
}
} // namespace
class AccessibilityAuraLinuxBrowserTest : public AccessibilityBrowserTest {
public:
AccessibilityAuraLinuxBrowserTest() = default;
AccessibilityAuraLinuxBrowserTest(const AccessibilityAuraLinuxBrowserTest&) =
delete;
AccessibilityAuraLinuxBrowserTest& operator=(
const AccessibilityAuraLinuxBrowserTest&) = delete;
~AccessibilityAuraLinuxBrowserTest() override = default;
protected:
static bool HasObjectWithAtkRoleFrameInAncestry(AtkObject* object) {
while (object) {
if (atk_object_get_role(object) == ATK_ROLE_FRAME) {
return true;
}
object = atk_object_get_parent(object);
}
return false;
}
// Ensures that the text and the start and end offsets retrieved using
// get_textAtOffset match the expected values.
static void CheckTextAtOffset(AtkText* text_object,
int offset,
AtkTextBoundary boundary_type,
int expected_start_offset,
int expected_end_offset,
const char* expected_text);
// Loads a page with an input text field and places sample text in it.
// Returns a pointer to the field's AtkText interface.
AtkText* SetUpInputField();
// Loads a page with a textarea text field, places sample text in it, and
// places the caret after the last character.
// Returns a pointer to the field's AtkText interface.
AtkText* SetUpTextareaField();
// Does a few checks on the scrollable input field and returns
// a pointer to the field's AtkText interface.
AtkText* GetScrollableInputField();
// Loads a page with a paragraph of sample text and returns its AtkText
// interface.
AtkText* SetUpSampleParagraph();
// Retrieves a pointer to the already loaded paragraph's AtkText interface.
AtkText* GetSampleParagraph();
// Searches the accessibility tree in pre-order debth-first traversal for a
// node with the given role and returns its AtkText interface if found,
// otherwise returns nullptr.
AtkText* FindNode(const AtkRole role);
void TestCharacterExtentsInScrollableInput();
private:
// Searches the accessibility tree in pre-order debth-first traversal starting
// at a given node and for a node with the given role and returns its AtkText
// interface if found, otherwise returns nullptr.
AtkText* FindNode(AtkObject* root, const AtkRole role) const;
};
void AccessibilityAuraLinuxBrowserTest::CheckTextAtOffset(
AtkText* text_object,
int offset,
AtkTextBoundary boundary_type,
int expected_start_offset,
int expected_end_offset,
const char* expected_text) {
::testing::Message message;
message << "While checking at index \'" << offset << "\' for \'"
<< expected_text << "\' at " << expected_start_offset << '-'
<< expected_end_offset << '.';
SCOPED_TRACE(message);
int start_offset = 0;
int end_offset = 0;
char* text = atk_text_get_text_at_offset(text_object, offset, boundary_type,
&start_offset, &end_offset);
EXPECT_EQ(expected_start_offset, start_offset);
EXPECT_EQ(expected_end_offset, end_offset);
EXPECT_STREQ(expected_text, text);
g_free(text);
}
AtkText* AccessibilityAuraLinuxBrowserTest::SetUpInputField() {
LoadInputField();
return FindNode(ATK_ROLE_ENTRY);
}
AtkText* AccessibilityAuraLinuxBrowserTest::SetUpTextareaField() {
LoadTextareaField();
return FindNode(ATK_ROLE_ENTRY);
}
AtkText* AccessibilityAuraLinuxBrowserTest::SetUpSampleParagraph() {
LoadSampleParagraph();
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
int number_of_children = atk_object_get_n_accessible_children(document);
EXPECT_LT(0, number_of_children);
// The input field is always the last child.
AtkObject* input = atk_object_ref_accessible_child(document, 0);
EXPECT_EQ(ATK_ROLE_PARAGRAPH, atk_object_get_role(input));
EXPECT_TRUE(ATK_IS_TEXT(input));
return ATK_TEXT(input);
}
AtkText* AccessibilityAuraLinuxBrowserTest::GetSampleParagraph() {
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
int number_of_children = atk_object_get_n_accessible_children(document);
EXPECT_LT(0, number_of_children);
// The paragraph is the last child.
AtkObject* paragraph = atk_object_ref_accessible_child(document, 0);
EXPECT_EQ(ATK_ROLE_PARAGRAPH, atk_object_get_role(paragraph));
EXPECT_TRUE(ATK_IS_TEXT(paragraph));
return ATK_TEXT(paragraph);
}
AtkText* AccessibilityAuraLinuxBrowserTest::GetScrollableInputField() {
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
AtkObject* div = atk_object_ref_accessible_child(document, 0);
EXPECT_NE(div, nullptr);
int n_div_children = atk_object_get_n_accessible_children(div);
EXPECT_GT(n_div_children, 0);
// The input field is always the last child.
AtkObject* input = atk_object_ref_accessible_child(div, n_div_children - 1);
EXPECT_EQ(ATK_ROLE_ENTRY, atk_object_get_role(input));
// Retrieve the IAccessibleText interface for the field.
AtkText* atk_text_field = ATK_TEXT(input);
// Set the caret before the last character.
AccessibilityNotificationWaiter waiter(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
long caret_offset = InputContentsString().size() - 1;
ExecuteScript(base::ASCIIToUTF16(
base::StrCat({"let textField = document.querySelector('input,textarea');"
"textField.focus();"
"textField.setSelectionRange(",
base::NumberToString(caret_offset), ",",
base::NumberToString(caret_offset),
");"
"textField.scrollLeft = 1000;"})));
EXPECT_TRUE(waiter.WaitForNotification());
g_object_unref(div);
return atk_text_field;
}
AtkText* AccessibilityAuraLinuxBrowserTest::FindNode(const AtkRole role) {
AtkObject* document = GetRendererAccessible();
EXPECT_NE(nullptr, document);
return FindNode(document, role);
}
AtkText* AccessibilityAuraLinuxBrowserTest::FindNode(AtkObject* root,
const AtkRole role) const {
EXPECT_NE(nullptr, root);
if (atk_object_get_role(root) == role) {
EXPECT_TRUE(ATK_IS_TEXT(root));
g_object_ref(root);
AtkText* root_text = ATK_TEXT(root);
return root_text;
}
for (int i = 0; i < atk_object_get_n_accessible_children(root); ++i) {
AtkObject* child = atk_object_ref_accessible_child(root, i);
EXPECT_NE(nullptr, child);
if (atk_object_get_role(child) == role) {
EXPECT_TRUE(ATK_IS_TEXT(child));
AtkText* child_text = ATK_TEXT(child);
return child_text;
}
if (AtkText* descendant_text = FindNode(child, role)) {
g_object_unref(child);
return descendant_text;
}
g_object_unref(child);
}
return nullptr;
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
AuraLinuxBrowserAccessibleParent) {
LoadInitialAccessibilityTreeFromHtml({});
// Get the BrowserAccessibilityManager.
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ui::BrowserAccessibilityManager* manager =
web_contents->GetRootBrowserAccessibilityManager();
ASSERT_NE(nullptr, manager);
auto* host_view = static_cast<RenderWidgetHostViewAura*>(
web_contents->GetRenderWidgetHostView());
ASSERT_NE(nullptr, host_view->GetNativeViewAccessible());
AtkObject* host_view_parent =
host_view->AccessibilityGetNativeViewAccessible();
ASSERT_NE(nullptr, host_view_parent);
ASSERT_TRUE(
AccessibilityAuraLinuxBrowserTest::HasObjectWithAtkRoleFrameInAncestry(
host_view_parent));
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestTextAtOffsetWithBoundaryCharacterAndEmbeddedObject) {
LoadInitialAccessibilityTreeFromHtml(R"HTML(<!DOCTYPE html>
<div contenteditable>
Before<img alt="image">after.
</div>
)HTML");
AtkObject* document = GetRendererAccessible();
ASSERT_EQ(1, atk_object_get_n_accessible_children(document));
AtkObject* contenteditable = atk_object_ref_accessible_child(document, 0);
ASSERT_NE(nullptr, contenteditable);
ASSERT_EQ(ATK_ROLE_SECTION, atk_object_get_role(contenteditable));
ASSERT_TRUE(ATK_IS_TEXT(contenteditable));
AtkText* contenteditable_text = ATK_TEXT(contenteditable);
int character_count = atk_text_get_character_count(contenteditable_text);
ASSERT_EQ(13, character_count);
const std::u16string embedded_character(
1, ui::AXPlatformNodeAuraLinux::kEmbeddedCharacter);
const std::vector<std::string> expected_hypertext = {
"B", "e", "f", "o", "r", "e", base::UTF16ToUTF8(embedded_character),
"a", "f", "t", "e", "r", "."};
// "Before".
//
// The embedded object character representing the image is at offset 6.
for (int i = 0; i < 6; ++i) {
CheckTextAtOffset(contenteditable_text, i, ATK_TEXT_BOUNDARY_CHAR, i,
(i + 1), expected_hypertext[i].c_str());
}
// "after.".
//
// Note that according to the ATK Spec, an offset that is equal to
// "character_count" is not permitted.
for (int i = 7; i < character_count; ++i) {
CheckTextAtOffset(contenteditable_text, i, ATK_TEXT_BOUNDARY_CHAR, i,
(i + 1), expected_hypertext[i].c_str());
}
ASSERT_EQ(3, atk_object_get_n_accessible_children(contenteditable));
// The image is the second child.
AtkObject* image = atk_object_ref_accessible_child(contenteditable, 1);
ASSERT_NE(nullptr, image);
ASSERT_EQ(ATK_ROLE_IMAGE, atk_object_get_role(image));
// The alt text of the image is not navigable as text.
ASSERT_FALSE(ATK_IS_TEXT(image));
g_object_unref(image);
g_object_unref(contenteditable_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestMultilingualTextAtOffsetWithBoundaryCharacter) {
AtkText* atk_text = SetUpInputField();
ASSERT_NE(nullptr, atk_text);
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ax::mojom::Event::kValueChanged);
// Place an e acute, and two emoticons in the text field.
ExecuteScript(
uR"SCRIPT(
const input = document.querySelector('input');
input.value =
'é👩\u200D❤\uFE0F\u200D👩🐶';
)SCRIPT");
ASSERT_TRUE(waiter.WaitForNotification());
int character_count = atk_text_get_character_count(atk_text);
// "character_count" is the number of actual characters, not the number of
// UTF16 code units.
//
// Currently, this doesn't properly count grapheme clusters, but it does
// handle surrogate pairs.
// TODO(nektar): Implement support for base::OffsetAdjuster in AXPosition.
ASSERT_EQ(9, character_count);
// The expected text consists of an e acute, and two emoticons, but
// not every multi-byte character is a surrogate pair.
CheckTextAtOffset(atk_text, 0, ATK_TEXT_BOUNDARY_CHAR, 0, 2,
base::WideToUTF8(L"e\x0301").c_str());
CheckTextAtOffset(atk_text, 1, ATK_TEXT_BOUNDARY_CHAR, 0, 2,
base::WideToUTF8(L"e\x0301").c_str());
CheckTextAtOffset(atk_text, 2, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 3, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 4, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 5, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 6, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 7, ATK_TEXT_BOUNDARY_CHAR, 2, 8,
"\xF0\x9F\x91\xA9\xE2\x80\x8D\xE2\x9D\xA4\xEF\xB8\x8F\xE2"
"\x80\x8D\xF0\x9F\x91\xA9");
CheckTextAtOffset(atk_text, 8, ATK_TEXT_BOUNDARY_CHAR, 8, 9,
"\xF0\x9F\x90\xB6");
CheckTextAtOffset(atk_text, 9, ATK_TEXT_BOUNDARY_CHAR, 0, 0, nullptr);
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestTextAtOffsetWithBoundaryLine) {
AtkText* atk_text = SetUpInputField();
// Single line text fields should return the whole text.
CheckTextAtOffset(atk_text, 0, ATK_TEXT_BOUNDARY_LINE_START, 0,
static_cast<int>(InputContentsString().size()),
InputContentsString().c_str());
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestMultiLineTextAtOffsetWithBoundaryLine) {
AtkText* atk_text = SetUpTextareaField();
CheckTextAtOffset(atk_text, 0, ATK_TEXT_BOUNDARY_LINE_START, 0, 24,
"Moz/5.0 (ST 6.x; WWW33)\n");
// If the offset is at the newline, return the line preceding it.
CheckTextAtOffset(atk_text, 31, ATK_TEXT_BOUNDARY_LINE_START, 24, 32,
"WebKit \n");
// Last line does not have a trailing newline.
CheckTextAtOffset(atk_text, 32, ATK_TEXT_BOUNDARY_LINE_START, 32,
static_cast<int>(InputContentsString().size()),
"\"KHTML, like\".");
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestBlankLineTextAtOffsetWithBoundaryLine) {
AtkText* atk_text = SetUpTextareaField();
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ax::mojom::Event::kValueChanged);
// Add a blank line at the end of the textarea.
ExecuteScript(
uR"SCRIPT(
const textarea = document.querySelector('textarea');
textarea.value += '\n';
)SCRIPT");
ASSERT_TRUE(waiter.WaitForNotification());
// The second last line should have an additional trailing newline. Also,
// Blink represents the blank line with a newline character, so in total there
// should be two more newlines. The second newline is not part of the HTML
// value attribute however.
int contents_string_length =
static_cast<int>(InputContentsString().size()) + 1;
CheckTextAtOffset(atk_text, 32, ATK_TEXT_BOUNDARY_LINE_START, 32,
contents_string_length, "\"KHTML, like\".\n");
CheckTextAtOffset(atk_text, 46, ATK_TEXT_BOUNDARY_LINE_START, 32,
contents_string_length, "\"KHTML, like\".\n");
// An offset one past the last character should return the last line which is
// blank. This is represented by Blink with yet another line break.
CheckTextAtOffset(atk_text, contents_string_length,
ATK_TEXT_BOUNDARY_LINE_START, contents_string_length,
(contents_string_length + 1), "\n");
{
// There should be no text after the blank line.
int start_offset = 0;
int end_offset = 0;
char* text = atk_text_get_text_at_offset(
atk_text, (contents_string_length + 1), ATK_TEXT_BOUNDARY_LINE_START,
&start_offset, &end_offset);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(0, end_offset);
EXPECT_EQ(nullptr, text);
}
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestParagraphTextAtOffsetWithBoundaryLine) {
AtkText* atk_text = SetUpSampleParagraph();
// There should be two lines in this paragraph.
const int newline_offset = 46;
int n_characters = atk_text_get_character_count(atk_text);
ASSERT_LT(newline_offset, n_characters);
const std::u16string embedded_character(
1, ui::AXPlatformNodeAuraLinux::kEmbeddedCharacter);
std::string expected_string = "Game theory is \"the study of " +
base::UTF16ToUTF8(embedded_character) +
" of conflict and\n";
for (int i = 0; i <= newline_offset; ++i) {
CheckTextAtOffset(atk_text, i, ATK_TEXT_BOUNDARY_LINE_START, 0,
newline_offset + 1, expected_string.c_str());
}
for (int i = newline_offset + 1; i < n_characters; ++i) {
CheckTextAtOffset(
atk_text, i, ATK_TEXT_BOUNDARY_LINE_START, newline_offset + 1,
n_characters,
"cooperation between intelligent rational decision-makers.\"");
}
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
DISABLED_TestParagraphTextAtOffsetWithBoundarySentence) {
LoadInitialAccessibilityTreeFromHtml(std::string(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div>One sentence. Two sentences. Three sentences!</div>
</body>
</html>)HTML"));
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
AtkText* div_element = ATK_TEXT(atk_object_ref_accessible_child(document, 0));
EXPECT_EQ(1, atk_object_get_n_accessible_children(ATK_OBJECT(div_element)));
AtkText* atk_text =
ATK_TEXT(atk_object_ref_accessible_child(ATK_OBJECT(div_element), 0));
int first_sentence_offset = 14;
int second_sentence_offset = first_sentence_offset + 15;
int third_sentence_offset = second_sentence_offset + 16;
for (int i = 0; i < first_sentence_offset; ++i) {
CheckTextAtOffset(atk_text, i, ATK_TEXT_BOUNDARY_SENTENCE_START, 0,
first_sentence_offset, "One sentence. ");
}
for (int i = first_sentence_offset + 1; i < second_sentence_offset; ++i) {
CheckTextAtOffset(atk_text, i, ATK_TEXT_BOUNDARY_SENTENCE_START,
first_sentence_offset, second_sentence_offset,
"Two sentences. ");
}
for (int i = second_sentence_offset + 1; i < third_sentence_offset; ++i) {
CheckTextAtOffset(atk_text, i, ATK_TEXT_BOUNDARY_SENTENCE_START,
second_sentence_offset, third_sentence_offset,
"Three sentences!");
}
g_object_unref(atk_text);
g_object_unref(div_element);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCharacterExtentsWithInvalidArguments) {
AtkText* atk_text = SetUpSampleParagraph();
int invalid_offset = -3;
int x = -1, y = -1;
int width = -1, height = -1;
// When given invalid arguments, atk_text_get_character_extents returns 0
// before 2.35.1 and -1 after:
// https://gitlab.gnome.org/GNOME/atk/-/merge_requests/44#9f621eb5fd3bcb2fa5c7bd228c9b1ad42edc46c8_32_33
// https://gnome.pages.gitlab.gnome.org/at-spi2-core/atk/AtkText.html#atk-text-get-character-extents
base::Version atk_version(atk_get_version());
int expect = atk_version.CompareTo(base::Version("2.35.1")) >= 0 ? -1 : 0;
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_SCREEN);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_PARENT);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_WINDOW);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
int n_characters = atk_text_get_character_count(atk_text);
ASSERT_LT(0, n_characters);
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_SCREEN);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_PARENT);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
atk_text_get_character_extents(atk_text, invalid_offset, &x, &y, &width,
&height, ATK_XY_WINDOW);
EXPECT_EQ(expect, x);
EXPECT_EQ(expect, y);
EXPECT_EQ(expect, width);
EXPECT_EQ(expect, height);
g_object_unref(atk_text);
}
AtkCoordType kCoordinateTypes[] = {
ATK_XY_SCREEN,
ATK_XY_WINDOW,
ATK_XY_PARENT,
};
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCharacterExtentsInEditable) {
AtkText* atk_text = SetUpSampleParagraph();
constexpr int newline_offset = 46;
int n_characters = atk_text_get_character_count(atk_text);
ASSERT_EQ(105, n_characters);
int x, y, width, height;
int previous_x, previous_y, previous_height;
for (AtkCoordType coordinate_type : kCoordinateTypes) {
atk_text_get_character_extents(atk_text, 0, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(0, x) << "at offset 0";
EXPECT_LT(0, y) << "at offset 0";
EXPECT_LT(1, width) << "at offset 0";
EXPECT_LT(1, height) << "at offset 0";
gfx::Rect combined_extents(x, y, width, height);
for (int offset = 1; offset < newline_offset; ++offset) {
::testing::Message message;
message << "While checking at offset " << offset;
SCOPED_TRACE(message);
previous_x = x;
previous_y = y;
previous_height = height;
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(previous_x, x);
EXPECT_EQ(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
combined_extents.Union(gfx::Rect(x, y, width, height));
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
AtkTextRectangle atk_rect;
atk_text_get_range_extents(atk_text, 0, offset + 1, coordinate_type,
&atk_rect);
EXPECT_EQ(combined_extents.x(), atk_rect.x);
EXPECT_EQ(combined_extents.y(), atk_rect.y);
EXPECT_EQ(combined_extents.width(), atk_rect.width);
EXPECT_EQ(combined_extents.height(), atk_rect.height);
}
{
::testing::Message message;
message << "While checking at offset " << newline_offset + 1;
SCOPED_TRACE(message);
atk_text_get_character_extents(atk_text, newline_offset + 1, &x, &y,
&width, &height, coordinate_type);
EXPECT_LE(0, x);
EXPECT_GT(previous_x, x);
EXPECT_LT(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
}
combined_extents = gfx::Rect(x, y, width, height);
for (int offset = newline_offset + 2; offset < n_characters; ++offset) {
::testing::Message message;
message << "While checking at offset " << offset;
SCOPED_TRACE(message);
previous_x = x;
previous_y = y;
previous_height = height;
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(previous_x, x);
EXPECT_EQ(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
combined_extents.Union(gfx::Rect(x, y, width, height));
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
AtkTextRectangle atk_rect;
atk_text_get_range_extents(atk_text, newline_offset + 1, offset + 1,
coordinate_type, &atk_rect);
EXPECT_EQ(combined_extents.x(), atk_rect.x);
EXPECT_EQ(combined_extents.y(), atk_rect.y);
EXPECT_EQ(combined_extents.width(), atk_rect.width);
EXPECT_EQ(combined_extents.height(), atk_rect.height);
}
}
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCharacterExtentsInScrollableEditable) {
LoadSampleParagraphInScrollableEditable();
// By construction, only the first line of the content editable is visible.
AtkText* atk_text = GetSampleParagraph();
constexpr int first_line_end = 5;
constexpr int last_line_start = 8;
int n_characters = atk_text_get_character_count(atk_text);
ASSERT_EQ(13, n_characters);
int x, y, width, height;
int previous_x, previous_y, previous_height;
for (AtkCoordType coordinate_type : kCoordinateTypes) {
// Test that non offscreen characters have increasing x coordinates and a
// height that is greater than 1px.
{
::testing::Message message;
message << "While checking at offset 0";
SCOPED_TRACE(message);
atk_text_get_character_extents(atk_text, 0, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(0, x);
EXPECT_LT(0, y);
EXPECT_LT(1, width);
EXPECT_LT(1, height);
}
for (int offset = 1; offset < first_line_end; ++offset) {
::testing::Message message;
message << "While checking at offset " << offset;
SCOPED_TRACE(message);
previous_x = x;
previous_y = y;
previous_height = height;
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(previous_x, x);
EXPECT_EQ(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
}
{
::testing::Message message;
message << "While checking at offset " << last_line_start;
SCOPED_TRACE(message);
atk_text_get_character_extents(atk_text, last_line_start, &x, &y, &width,
&height, coordinate_type);
EXPECT_LT(0, x);
EXPECT_LT(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
}
for (int offset = last_line_start + 1; offset < n_characters; ++offset) {
::testing::Message message;
message << "While checking at offset " << offset;
SCOPED_TRACE(message);
previous_x = x;
previous_y = y;
atk_text_get_character_extents(atk_text, offset, &x, &y, &width, &height,
coordinate_type);
EXPECT_LT(previous_x, x);
EXPECT_EQ(previous_y, y);
EXPECT_LT(1, width);
EXPECT_EQ(previous_height, height);
}
}
g_object_unref(atk_text);
}
void AccessibilityAuraLinuxBrowserTest::
TestCharacterExtentsInScrollableInput() {
AtkText* input_text = GetScrollableInputField();
int contents_string_length = InputContentsString().length();
int visible_characters_start = 21;
int n_characters = atk_text_get_character_count(input_text);
EXPECT_EQ(contents_string_length, n_characters);
int caret_offset = atk_text_get_caret_offset(input_text);
EXPECT_EQ(contents_string_length - 1, caret_offset);
int x, y, width, height;
int previous_x, previous_y, previous_height;
for (int coordinate = ATK_XY_SCREEN; coordinate <= ATK_XY_WINDOW;
++coordinate) {
auto coordinate_type = static_cast<AtkCoordType>(coordinate);
atk_text_get_character_extents(input_text, 0, &x, &y, &width, &height,
coordinate_type);
EXPECT_GT(0, x + width) << "at offset 0";
EXPECT_LT(0, y) << "at offset 0";
EXPECT_LT(1, width) << "at offset 0";
EXPECT_LT(1, height) << "at offset 0";
for (int offset = 1; offset < (visible_characters_start - 1); ++offset) {
previous_x = x;
previous_y = y;
previous_height = height;
atk_text_get_character_extents(input_text, offset, &x, &y, &width,
&height, coordinate_type);
EXPECT_LT(previous_x, x) << "at offset " << offset;
EXPECT_EQ(previous_y, y) << "at offset " << offset;
EXPECT_LT(1, width) << "at offset " << offset;
EXPECT_EQ(previous_height, height) << "at offset " << offset;
}
// Test that non offscreen characters have increasing x coordinates and a
// width that is greater than 1px.
atk_text_get_character_extents(input_text, visible_characters_start, &x, &y,
&width, &height, coordinate_type);
EXPECT_LT(previous_x, x) << "at offset " << visible_characters_start;
EXPECT_EQ(previous_y, y) << "at offset " << visible_characters_start;
EXPECT_LT(1, width) << "at offset " << visible_characters_start;
EXPECT_EQ(previous_height, height)
<< "at offset " << visible_characters_start;
// Exclude the dot at the end of the text field, because it has a width of
// one anyway.
for (int offset = visible_characters_start + 1; offset < (n_characters - 1);
++offset) {
previous_x = x;
previous_y = y;
previous_height = height;
atk_text_get_character_extents(input_text, offset, &x, &y, &width,
&height, coordinate_type);
EXPECT_LT(previous_x, x) << "at offset " << offset;
EXPECT_EQ(previous_y, y) << "at offset " << offset;
EXPECT_LT(1, width) << "at offset " << offset;
EXPECT_EQ(previous_height, height) << "at offset " << offset;
}
}
g_object_unref(input_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCharacterExtentsInScrollableInputField) {
LoadScrollableInputField("text");
TestCharacterExtentsInScrollableInput();
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCharacterExtentsInScrollableInputTypeSearchField) {
LoadScrollableInputField("search");
TestCharacterExtentsInScrollableInput();
}
typedef bool (*ScrollToPointFunc)(AtkComponent* component,
AtkCoordType coords,
gint x,
gint y);
typedef bool (*ScrollToFunc)(AtkComponent* component, AtkScrollType type);
// TODO(crbug.com/40866728): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
DISABLED_TestScrollToPoint) {
LoadSampleParagraphInScrollableDocument();
AtkText* atk_text = GetSampleParagraph();
ASSERT_TRUE(ATK_IS_COMPONENT(atk_text));
AtkComponent* atk_component = ATK_COMPONENT(atk_text);
int prev_x, prev_y, x, y;
atk_component_get_extents(atk_component, &prev_x, &prev_y, nullptr, nullptr,
ATK_XY_SCREEN);
AccessibilityNotificationWaiter location_changed_waiter(
shell()->web_contents(), ax::mojom::Event::kLocationChanged);
atk_component_scroll_to_point(atk_component, ATK_XY_PARENT, 0, 0);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_component_get_extents(atk_component, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(prev_x, x);
EXPECT_GT(prev_y, y);
constexpr int kScrollToY = 0;
atk_component_scroll_to_point(atk_component, ATK_XY_SCREEN, 0, kScrollToY);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_component_get_extents(atk_component, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(kScrollToY, y);
constexpr int kScrollToY_2 = 243;
atk_component_scroll_to_point(atk_component, ATK_XY_SCREEN, 0, kScrollToY_2);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_component_get_extents(atk_component, nullptr, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(kScrollToY_2, y);
atk_component_scroll_to_point(atk_component, ATK_XY_SCREEN, 0, 129);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_component_get_extents(atk_component, nullptr, &y, nullptr, nullptr,
ATK_XY_SCREEN);
AtkObject* frame = FindAtkObjectParentFrame(ATK_OBJECT(atk_component));
int frame_y;
atk_component_get_extents(ATK_COMPONENT(frame), nullptr, &frame_y, nullptr,
nullptr, ATK_XY_SCREEN);
EXPECT_EQ(frame_y, y);
g_object_unref(atk_text);
}
// TODO(crbug.com/40866728): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
DISABLED_TestScrollTo) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div style="height: 5000px; width: 5000px;"></div>
<img style="display: relative; left: 1000px;" alt="Target1">
<div style="height: 5000px;"></div>
<img style="display: relative; left: 1000px;" alt="Target2">
<div style="height: 5000px;"></div>
<span>Target 3</span>
<div style="height: 5000px;"></div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
// Get the dimensions of the document.
int doc_x, doc_y, doc_width, doc_height;
atk_component_get_extents(ATK_COMPONENT(document), &doc_x, &doc_y, &doc_width,
&doc_height, ATK_XY_SCREEN);
// The document should only have three children, two img elements
// and a single span element.
ASSERT_EQ(3, atk_object_get_n_accessible_children(document));
AtkObject* target = atk_object_ref_accessible_child(document, 0);
AtkObject* target2 = atk_object_ref_accessible_child(document, 1);
AtkObject* target3 = atk_object_ref_accessible_child(document, 2);
ASSERT_TRUE(ATK_IS_COMPONENT(target));
ASSERT_TRUE(ATK_IS_COMPONENT(target2));
ASSERT_TRUE(ATK_IS_COMPONENT(target3));
ASSERT_EQ(ATK_ROLE_IMAGE, atk_object_get_role(target));
ASSERT_EQ(ATK_ROLE_IMAGE, atk_object_get_role(target2));
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kScrollPositionChanged);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target), ATK_SCROLL_TOP_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
int x, y;
atk_component_get_extents(ATK_COMPONENT(target), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_NE(x, doc_x);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target), ATK_SCROLL_TOP_LEFT));
ASSERT_TRUE(waiter.WaitForNotification());
atk_component_get_extents(ATK_COMPONENT(target), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target), ATK_SCROLL_BOTTOM_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_component_get_extents(ATK_COMPONENT(target), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target), ATK_SCROLL_RIGHT_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_component_get_extents(ATK_COMPONENT(target), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_NE(x, doc_x);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target2), ATK_SCROLL_LEFT_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_component_get_extents(ATK_COMPONENT(target2), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(
atk_component_scroll_to(ATK_COMPONENT(target2), ATK_SCROLL_TOP_LEFT));
ASSERT_TRUE(waiter.WaitForNotification());
atk_component_get_extents(ATK_COMPONENT(target2), &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_EQ(x, doc_x);
// Orca expects atk_text_set_caret_offset to operate like scroll to the
// target node like atk_component_scroll_to, so we test that here.
ASSERT_TRUE(ATK_IS_TEXT(target3));
AccessibilityNotificationWaiter waiter3(
shell()->web_contents(), ax::mojom::Event::kScrollPositionChanged);
atk_text_set_caret_offset(ATK_TEXT(target3), 0);
ASSERT_TRUE(waiter3.WaitForNotification());
// The text area should be scrolled to somewhere near the bottom of the
// screen. We check that it is in the bottom quarter of the screen here,
// but still fully onscreen.
int height;
atk_component_get_extents(ATK_COMPONENT(target3), nullptr, &y, nullptr,
&height, ATK_XY_SCREEN);
int doc_bottom = doc_y + doc_height;
int bottom_third = doc_bottom - (static_cast<float>(doc_height) / 3.0);
EXPECT_GT(y, bottom_third);
EXPECT_LT(y, doc_bottom - height + 1);
g_object_unref(target);
g_object_unref(target2);
g_object_unref(target3);
}
// TODO(crbug.com/40866728): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
DISABLED_TestScrollSubstringTo) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div style="height: 50000px; width: 50000px;"></div>
<div style="position: relative; left: 1000px;">Target 1</div>
<div style="height: 50000px;"></div>
<div style="position: relative; left: 10000px;"> Target 2</div>
<div style="height: 50000px;"></div>
<div style="position: relative; left: 100000px;"> Target 3</div>
<div style="height: 50000px;"></div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
// Get the dimensions of the document.
int doc_x, doc_y, doc_width, doc_height;
atk_component_get_extents(ATK_COMPONENT(document), &doc_x, &doc_y, &doc_width,
&doc_height, ATK_XY_SCREEN);
// The document should only have three children, three span elements.
ASSERT_EQ(3, atk_object_get_n_accessible_children(document));
AtkObject* target1 = atk_object_ref_accessible_child(document, 0);
ASSERT_TRUE(ATK_IS_TEXT(target1));
ASSERT_EQ(ATK_ROLE_SECTION, atk_object_get_role(target1));
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kScrollPositionChanged);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_TOP_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
int x, y;
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_NE(x, doc_x);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_TOP_LEFT));
ASSERT_TRUE(waiter.WaitForNotification());
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_BOTTOM_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_RIGHT_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_NE(x, doc_x);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_LEFT_EDGE));
ASSERT_TRUE(waiter.WaitForNotification());
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_NE(y, doc_y);
EXPECT_EQ(x, doc_x);
ASSERT_TRUE(atk_text_scroll_substring_to(ATK_TEXT(target1), 1, 2,
ATK_SCROLL_TOP_LEFT));
ASSERT_TRUE(waiter.WaitForNotification());
atk_text_get_character_extents(ATK_TEXT(target1), 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(y, doc_y);
EXPECT_EQ(x, doc_x);
g_object_unref(target1);
}
// TODO(crbug.com/40866728): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
DISABLED_TestScrollSubstringToPoint) {
LoadSampleParagraphInScrollableDocument();
AtkText* atk_text = GetSampleParagraph();
ASSERT_TRUE(ATK_IS_COMPONENT(atk_text));
AtkComponent* atk_component = ATK_COMPONENT(atk_text);
int prev_x, prev_y, x, y;
atk_text_get_character_extents(atk_text, 1, &prev_x, &prev_y, nullptr,
nullptr, ATK_XY_SCREEN);
AccessibilityNotificationWaiter location_changed_waiter(
shell()->web_contents(), ax::mojom::Event::kLocationChanged);
atk_text_scroll_substring_to_point(atk_text, 1, 2, ATK_XY_PARENT, 0, 0);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_text_get_character_extents(atk_text, 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(prev_x, x);
EXPECT_GT(prev_y, y);
constexpr int kScrollToY = 0;
atk_text_scroll_substring_to_point(atk_text, 1, 2, ATK_XY_SCREEN, 0,
kScrollToY);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_text_get_character_extents(atk_text, 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(kScrollToY, y);
constexpr int kScrollToY_2 = 243;
atk_text_scroll_substring_to_point(atk_text, 1, 2, ATK_XY_SCREEN, 0,
kScrollToY_2);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_text_get_character_extents(atk_text, 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
EXPECT_EQ(kScrollToY_2, y);
atk_text_scroll_substring_to_point(atk_text, 1, 2, ATK_XY_SCREEN, 0, 129);
ASSERT_TRUE(location_changed_waiter.WaitForNotification());
atk_text_get_character_extents(atk_text, 1, &x, &y, nullptr, nullptr,
ATK_XY_SCREEN);
AtkObject* frame = FindAtkObjectParentFrame(ATK_OBJECT(atk_component));
int frame_y;
atk_component_get_extents(ATK_COMPONENT(frame), nullptr, &frame_y, nullptr,
nullptr, ATK_XY_SCREEN);
// We do a check that the vertical position is within 5 pixels of the frame
// position.
EXPECT_LT(std::abs(frame_y - y), 5);
g_object_unref(atk_text);
}
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
// Flaky on crbug.com/1026149
#define MAYBE_TestSetSelection DISABLED_TestSetSelection
#else
#define MAYBE_TestSetSelection TestSetSelection
#endif
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
MAYBE_TestSetSelection) {
AtkText* atk_text = SetUpInputField();
int start_offset, end_offset;
gchar* selected_text =
atk_text_get_selection(atk_text, 0, &start_offset, &end_offset);
EXPECT_EQ(nullptr, selected_text);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(0, end_offset);
AccessibilityNotificationWaiter waiter(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
int contents_string_length = static_cast<int>(InputContentsString().size());
start_offset = 0;
end_offset = contents_string_length;
EXPECT_TRUE(atk_text_set_selection(atk_text, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_text, 0, &start_offset, &end_offset);
EXPECT_NE(nullptr, selected_text);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(contents_string_length, end_offset);
g_free(selected_text);
start_offset = contents_string_length;
end_offset = 1;
EXPECT_TRUE(atk_text_set_selection(atk_text, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_text, 0, &start_offset, &end_offset);
EXPECT_NE(nullptr, selected_text);
EXPECT_EQ(1, start_offset);
EXPECT_EQ(contents_string_length, end_offset);
g_free(selected_text);
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SetSelectionWithIgnoredObjects) {
LoadInitialAccessibilityTreeFromHtml(R"HTML(<!DOCTYPE html>
<html>
<body>
<ul>
<li>
<div role="presentation"></div>
<p role="presentation">
<span>Banana</span>
</p>
<span>fruit.</span>
</li>
</ul>
</body>
</html>)HTML");
AtkText* atk_list_item = FindNode(ATK_ROLE_LIST_ITEM);
ASSERT_NE(nullptr, atk_list_item);
// The hypertext expose by "list_item_text" includes a bullet (U+2022)
// followed by a space for the list bullet and the joined word "Bananafruit.".
// The word "Banana" is exposed as text because its container paragraph is
// ignored.
int n_characters = atk_text_get_character_count(atk_list_item);
ASSERT_EQ(14, n_characters);
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
// First select the whole of the text found in the hypertext.
int start_offset = 0;
int end_offset = n_characters;
std::string bullet = base::UTF16ToUTF8(kString16Bullet);
char* selected_text = nullptr;
EXPECT_TRUE(
atk_text_set_selection(atk_list_item, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset);
ASSERT_NE(nullptr, selected_text);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(n_characters, end_offset);
// The list bullet should be represented by a bullet character (U+2022)
// followed by a space.
EXPECT_STREQ((bullet + std::string("Bananafruit.")).c_str(), selected_text);
g_free(selected_text);
// Select only the list bullet.
start_offset = 0;
end_offset = 2;
EXPECT_TRUE(
atk_text_set_selection(atk_list_item, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset);
ASSERT_NE(nullptr, selected_text);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(2, end_offset);
// The list bullet should be represented by a bullet character (U+2022)
// followed by a space.
EXPECT_STREQ(bullet.c_str(), selected_text);
g_free(selected_text);
// Select the word "Banana" in the ignored paragraph.
start_offset = 2;
end_offset = 8;
EXPECT_TRUE(
atk_text_set_selection(atk_list_item, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset);
ASSERT_NE(nullptr, selected_text);
EXPECT_EQ(2, start_offset);
EXPECT_EQ(8, end_offset);
EXPECT_STREQ("Banana", selected_text);
g_free(selected_text);
// Select both the list bullet and the word "Banana" in the ignored paragraph.
start_offset = 0;
end_offset = 8;
EXPECT_TRUE(
atk_text_set_selection(atk_list_item, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset);
ASSERT_NE(nullptr, selected_text);
EXPECT_EQ(0, start_offset);
EXPECT_EQ(8, end_offset);
// The list bullet should be represented by a bullet character (U+2022)
// followed by a space.
EXPECT_STREQ((bullet + std::string("Banana")).c_str(), selected_text);
g_free(selected_text);
// Select the joined word "Bananafruit." both in the ignored paragraph and in
// the unignored span.
start_offset = 2;
end_offset = n_characters;
EXPECT_TRUE(
atk_text_set_selection(atk_list_item, 0, start_offset, end_offset));
ASSERT_TRUE(waiter.WaitForNotification());
selected_text =
atk_text_get_selection(atk_list_item, 0, &start_offset, &end_offset);
ASSERT_NE(nullptr, selected_text);
EXPECT_EQ(2, start_offset);
EXPECT_EQ(n_characters, end_offset);
EXPECT_STREQ("Bananafruit.", selected_text);
g_free(selected_text);
g_object_unref(atk_list_item);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest, TestAtkTextListItem) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<li>Text 1</li>
<li>Text 2</li>
<li>Text 3</li>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(3, atk_object_get_n_accessible_children(document));
AtkObject* list_item_1 = atk_object_ref_accessible_child(document, 0);
AtkObject* list_item_2 = atk_object_ref_accessible_child(document, 1);
EXPECT_TRUE(ATK_IS_TEXT(list_item_1));
std::string expected_string = base::UTF16ToUTF8(kString16Bullet) + "Text 1";
// The text of the list item should include the list marker as a bullet char.
gchar* text = atk_text_get_text(ATK_TEXT(list_item_1), 0, -1);
EXPECT_STREQ(text, expected_string.c_str());
g_free(text);
text = atk_text_get_text_at_offset(
ATK_TEXT(list_item_2), 0, ATK_TEXT_BOUNDARY_WORD_START, nullptr, nullptr);
ASSERT_STREQ(text, base::UTF16ToUTF8(kString16Bullet).c_str());
g_free(text);
text = atk_text_get_text_at_offset(
ATK_TEXT(list_item_2), 2, ATK_TEXT_BOUNDARY_WORD_START, nullptr, nullptr);
ASSERT_STREQ(text, "Text ");
g_free(text);
g_object_unref(list_item_1);
g_object_unref(list_item_2);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestTextSelectionChangedDuplicateSignals) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div>
Sufficiently long div content
</div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
AtkObject* div = atk_object_ref_accessible_child(document, 0);
EXPECT_NE(div, nullptr);
int selection_changed_signals = 0;
g_signal_connect(div, "text-selection-changed",
G_CALLBACK(+[](AtkText*, int* count) { *count += 1; }),
&selection_changed_signals);
int caret_moved_signals = 0;
g_signal_connect(div, "text-caret-moved",
G_CALLBACK(+[](AtkText*, gint, int* count) { *count += 1; }),
&caret_moved_signals);
auto waiter = std::make_unique<AccessibilityNotificationWaiter>(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
atk_text_set_caret_offset(ATK_TEXT(div), 0);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_EQ(selection_changed_signals, 0);
ASSERT_EQ(caret_moved_signals, 1);
caret_moved_signals = selection_changed_signals = 0;
atk_text_set_selection(ATK_TEXT(div), 0, 0, 3);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_EQ(selection_changed_signals, 1);
ASSERT_EQ(caret_moved_signals, 1);
caret_moved_signals = selection_changed_signals = 0;
atk_text_set_caret_offset(ATK_TEXT(div), 3);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_EQ(selection_changed_signals, 1);
ASSERT_EQ(caret_moved_signals, 0);
caret_moved_signals = selection_changed_signals = 0;
atk_text_set_caret_offset(ATK_TEXT(div), 2);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_EQ(selection_changed_signals, 0);
ASSERT_EQ(caret_moved_signals, 1);
g_object_unref(div);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestTextSelectionAcrossElements) {
LoadInitialAccessibilityTreeFromHtml(std::string(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div id="parent" contenteditable="true">
<div id="child1">Child 1</div>
<div id="child2">Child 2</div>
</div>
</body>
</html>)HTML"));
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
AtkText* parent = ATK_TEXT(atk_object_ref_accessible_child(document, 0));
EXPECT_EQ(2, atk_object_get_n_accessible_children(ATK_OBJECT(parent)));
AtkText* child1 =
ATK_TEXT(atk_object_ref_accessible_child(ATK_OBJECT(parent), 0));
AtkText* child2 =
ATK_TEXT(atk_object_ref_accessible_child(ATK_OBJECT(parent), 1));
EXPECT_NE(nullptr, child1);
EXPECT_NE(nullptr, child2);
auto callback = G_CALLBACK(+[](AtkText*, bool* flag) { *flag = true; });
bool saw_selection_change_in_parent = false;
g_signal_connect(parent, "text-selection-changed", callback,
&saw_selection_change_in_parent);
bool saw_selection_change_in_child1 = false;
g_signal_connect(child1, "text-selection-changed", callback,
&saw_selection_change_in_child1);
bool saw_selection_change_in_child2 = false;
g_signal_connect(child2, "text-selection-changed", callback,
&saw_selection_change_in_child2);
AccessibilityNotificationWaiter selection_waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
ExecuteScript(
u"let parent = document.getElementById('parent');"
u"let child1 = document.getElementById('child1');"
u"let child2 = document.getElementById('child2');"
u"let range = document.createRange();"
u"range.setStart(child1.firstChild, 3);"
u"range.setEnd(child1.firstChild, 5);"
u"parent.focus();"
u"document.getSelection().removeAllRanges();"
u"document.getSelection().addRange(range);");
ASSERT_TRUE(selection_waiter.WaitForNotification());
EXPECT_FALSE(saw_selection_change_in_parent);
EXPECT_TRUE(saw_selection_change_in_child1);
EXPECT_FALSE(saw_selection_change_in_child2);
saw_selection_change_in_parent = false;
saw_selection_change_in_child1 = false;
saw_selection_change_in_child2 = false;
EXPECT_TRUE(atk_text_remove_selection(parent, 0));
ASSERT_TRUE(selection_waiter.WaitForNotification());
EXPECT_FALSE(saw_selection_change_in_parent);
EXPECT_TRUE(saw_selection_change_in_child1);
EXPECT_FALSE(saw_selection_change_in_child2);
saw_selection_change_in_parent = false;
saw_selection_change_in_child1 = false;
saw_selection_change_in_child2 = false;
ExecuteScript(
u"let range2 = document.createRange();"
u"range2.setStart(child1.firstChild, 0);"
u"range2.setEnd(child2.firstChild, 3);"
u"parent.focus();"
u"document.getSelection().removeAllRanges();"
u"document.getSelection().addRange(range2);");
ASSERT_TRUE(selection_waiter.WaitForNotification());
EXPECT_TRUE(saw_selection_change_in_parent);
EXPECT_FALSE(saw_selection_change_in_child1);
EXPECT_FALSE(saw_selection_change_in_child2);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SetCaretInTextWithGeneratedContent) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body contenteditable>
<style>h1.generated::before{content:" [ ";}</style>
<style>h1.generated::after{content:" ] ";}</style>
<h1 class="generated">Foo</h1>
</body>
</html>)HTML");
AtkObject* document = GetRendererAccessible();
AtkObject* contenteditable = atk_object_ref_accessible_child(document, 0);
ASSERT_NE(nullptr, contenteditable);
ASSERT_EQ(ATK_ROLE_SECTION, atk_object_get_role(contenteditable));
ASSERT_TRUE(ATK_IS_TEXT(contenteditable));
AtkObject* heading = atk_object_ref_accessible_child(contenteditable, 0);
ASSERT_NE(nullptr, heading);
ASSERT_EQ(atk_object_get_role(heading), ATK_ROLE_HEADING);
// The accessible text for the heading should match the rendered text and
// not the DOM text.
gchar* text = atk_text_get_text(ATK_TEXT(heading), 0, -1);
ASSERT_STREQ(text, "[ Foo ]");
g_free(text);
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
// Caret can't be set inside generated content, it will go to the closest
// allowed place. Ordered the targets so that the caret will always actually
// move somewhere between steps, and thus the waiter will always be satisfied.
std::vector<int> target_offset = {0, 3, 1, 4, 2, 4, 5, 4, 6};
std::vector<int> expect_offset = {2, 3, 2, 4, 2, 4, 2, 4, 2};
for (size_t i = 0; i < target_offset.size(); i++) {
atk_text_set_caret_offset(ATK_TEXT(heading), target_offset[i]);
ASSERT_TRUE(waiter.WaitForNotification());
ASSERT_EQ(expect_offset[i], atk_text_get_caret_offset(ATK_TEXT(heading)));
}
g_object_unref(heading);
g_object_unref(contenteditable);
}
// TODO(crbug.com/41469621): This flakes on linux.
IN_PROC_BROWSER_TEST_F(
AccessibilityAuraLinuxBrowserTest,
DISABLED_TestSetCaretSetsSequentialFocusNavigationStartingPoint) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div>
0
<a href="http://google.com">1</a>
2
<a href="http://google.com">3</a>
4
<a href="http://google.com">5</a>
6
<a href="http://google.com"><div>7</div></a>
8
</div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
AtkObject* child_2 = atk_object_ref_accessible_child(document, 2);
AtkObject* child_3 = atk_object_ref_accessible_child(document, 3);
AtkObject* child_7 = atk_object_ref_accessible_child(document, 7);
EXPECT_NE(child_2, nullptr);
EXPECT_NE(child_3, nullptr);
EXPECT_NE(child_7, nullptr);
// Move the caret to the "3" div. This should also set the sequential
// focus navigation starting point.
atk_text_set_caret_offset(ATK_TEXT(child_2), 0);
auto waiter = std::make_unique<AccessibilityNotificationWaiter>(
shell()->web_contents(), ax::mojom::Event::kFocus);
// Now send two tab presses to advance the focus.
// TODO(mrobinson): For some reason, in the test harness two tabs are
// necessary to advance focus after setting the selection (caret). This isn't
// necessary when running interactively.
SimulateKeyPress(shell()->web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
ASSERT_TRUE(waiter->WaitForNotification());
SimulateKeyPress(shell()->web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_TRUE(IsAtkObjectFocused(child_3));
// Now we repeat a similar test, but this time setting the caret offset on
// the document. In this case, the sequential navigation starting point
// should move to the appropriate child.
atk_text_set_caret_offset(ATK_TEXT(document), 13);
SimulateKeyPress(shell()->web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
ASSERT_TRUE(waiter->WaitForNotification());
SimulateKeyPress(shell()->web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_TRUE(IsAtkObjectFocused(child_7));
// Now test setting the caret in a node that can accept focus. That
// node should actually receive focus.
atk_text_set_caret_offset(ATK_TEXT(child_3), 0);
SimulateKeyPress(shell()->web_contents(), ui::DomKey::TAB, ui::DomCode::TAB,
ui::VKEY_TAB, false, false, false, false);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_TRUE(IsAtkObjectFocused(child_3));
AtkObject* link_section = atk_object_ref_accessible_child(child_7, 0);
EXPECT_NE(link_section, nullptr);
AtkObject* link_text = atk_object_ref_accessible_child(link_section, 0);
EXPECT_NE(link_text, nullptr);
atk_text_set_caret_offset(ATK_TEXT(link_text), 0);
ASSERT_TRUE(waiter->WaitForNotification());
ASSERT_TRUE(IsAtkObjectFocused(child_7));
g_object_unref(link_section);
g_object_unref(link_text);
g_object_unref(child_2);
g_object_unref(child_3);
g_object_unref(child_7);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SelectionTriggersReparentingOnSelectionStart) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<head>
<script>
document.onselectstart = () => {
var tomove = document.getElementById("tomove");
document.getElementById("div").appendChild(tomove);
}
</script>
</head>
<body>
<div id="tomove">Move me</div>
<p id="paragraph">hello world</p>
<div id="div"></div>
</body>
</html>)HTML");
AtkObject* document = GetRendererAccessible();
AtkObject* paragraph = atk_object_ref_accessible_child(document, 1);
ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 5));
ASSERT_TRUE(waiter.WaitForNotification());
gchar* selected =
atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
EXPECT_STREQ(selected, "hello");
g_free(selected);
g_object_unref(paragraph);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SelectionTriggersAnchorDeletionOnSelectionStart) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<head>
<script>
document.onselectstart = () => {
document.getElementById("p").removeChild(p.childNodes[0]);
}
</script>
</head>
<body>
<p id="p"><span>hello</span> <span>world</span></p>
<button id="button">ok</button>
</body>
</html>)HTML");
AtkObject* document = GetRendererAccessible();
AtkObject* paragraph = atk_object_ref_accessible_child(document, 0);
ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
AtkObject* button = atk_object_ref_accessible_child(document, 1);
ASSERT_EQ(atk_object_get_role(button), ATK_ROLE_PUSH_BUTTON);
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ax::mojom::Event::kFocus);
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 11));
atk_component_grab_focus(ATK_COMPONENT(button));
ASSERT_TRUE(waiter.WaitForNotification());
gchar* selected =
atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
EXPECT_STREQ(selected, nullptr);
g_free(selected);
g_object_unref(paragraph);
g_object_unref(button);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SelectionTriggersFocusDeletionOnSelectionStart) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<head>
<script>
document.onselectstart = () => {
document.getElementById("p").removeChild(p.childNodes[2]);
}
</script>
</head>
<body>
<p id="p"><span>hello</span> <span>world</span></p>
<button id="button">ok</button>
</body>
</html>)HTML");
AtkObject* document = GetRendererAccessible();
AtkObject* paragraph = atk_object_ref_accessible_child(document, 0);
ASSERT_EQ(atk_object_get_role(paragraph), ATK_ROLE_PARAGRAPH);
AtkObject* button = atk_object_ref_accessible_child(document, 1);
ASSERT_EQ(atk_object_get_role(button), ATK_ROLE_PUSH_BUTTON);
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ax::mojom::Event::kFocus);
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(paragraph), 0, 0, 11));
atk_component_grab_focus(ATK_COMPONENT(button));
ASSERT_TRUE(waiter.WaitForNotification());
gchar* selected =
atk_text_get_selection(ATK_TEXT(paragraph), 0, nullptr, nullptr);
EXPECT_STREQ(selected, nullptr);
g_free(selected);
g_object_unref(paragraph);
g_object_unref(button);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
SelectionTriggersReparentingOnFocus) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<head>
<script>
function go() {
var edit = document.getElementById("edit");
document.getElementById("search").appendChild(edit);
}
</script>
</head>
<body>
<span id="edit" tabindex="0" contenteditable onfocusin="go()">foo</span>
<div id="search" role="search"></div>
</body>
</html>)HTML");
AtkObject* document = GetRendererAccessible();
AtkObject* section = atk_object_ref_accessible_child(document, 0);
AtkObject* edit = atk_object_ref_accessible_child(section, 0);
ASSERT_TRUE(IsAtkObjectEditable(edit));
ASSERT_FALSE(IsAtkObjectFocused(edit));
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(edit), 0, 1, 2));
ASSERT_TRUE(waiter.WaitForNotification());
// When the unfocused contenteditable span has its selection set, focus will
// be set. That will trigger the script in the source to move that span to
// a different parent, causing focus to be removed and the selection cleared.
ASSERT_FALSE(IsAtkObjectFocused(edit));
gchar* selected = atk_text_get_selection(ATK_TEXT(edit), 0, nullptr, nullptr);
EXPECT_STREQ(selected, nullptr);
g_free(selected);
g_object_unref(edit);
g_object_unref(section);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestFocusInputTextFields) {
auto verify_selection = [](AtkObject* object, const char* selection) {
gchar* selected_text =
atk_text_get_selection(ATK_TEXT(object), 0, nullptr, nullptr);
EXPECT_STREQ(selected_text, selection);
g_free(selected_text);
int n_selections = atk_text_get_n_selections(ATK_TEXT(object));
EXPECT_EQ(n_selections, selection ? 1 : 0);
};
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div>
<input value="First Field">
<input value="Second Field">
<input value="Third Field">
<input value="Fourth Field">
</div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
AtkObject* container = atk_object_ref_accessible_child(document, 0);
AtkObject* field_1 = atk_object_ref_accessible_child(container, 0);
AtkObject* field_2 = atk_object_ref_accessible_child(container, 1);
AtkObject* field_3 = atk_object_ref_accessible_child(container, 2);
AtkObject* field_4 = atk_object_ref_accessible_child(container, 3);
EXPECT_NE(field_1, nullptr);
EXPECT_NE(field_2, nullptr);
EXPECT_NE(field_3, nullptr);
EXPECT_NE(field_4, nullptr);
auto waiter = std::make_unique<AccessibilityNotificationWaiter>(
shell()->web_contents(), ax::mojom::Event::kFocus);
atk_component_grab_focus(ATK_COMPONENT(field_1));
ASSERT_TRUE(waiter->WaitForNotification());
waiter = std::make_unique<AccessibilityNotificationWaiter>(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(field_1), 0, 0, 5));
ASSERT_TRUE(waiter->WaitForNotification());
EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(field_2), 0, 0, -1));
ASSERT_TRUE(waiter->WaitForNotification());
// Only the field that is currently focused should return a selection.
ASSERT_FALSE(IsAtkObjectFocused(field_1));
ASSERT_TRUE(IsAtkObjectFocused(field_2));
verify_selection(field_1, nullptr);
verify_selection(field_2, "Second Field");
verify_selection(field_3, nullptr);
verify_selection(field_4, nullptr);
waiter = std::make_unique<AccessibilityNotificationWaiter>(
shell()->web_contents(), ax::mojom::Event::kFocus);
atk_component_grab_focus(ATK_COMPONENT(field_1));
ASSERT_TRUE(waiter->WaitForNotification());
// Now that the focus has returned to the first field, it should return the
// original selection that we set on it.
ASSERT_TRUE(IsAtkObjectFocused(field_1));
ASSERT_FALSE(IsAtkObjectFocused(field_2));
verify_selection(field_1, "First");
verify_selection(field_2, nullptr);
verify_selection(field_3, nullptr);
verify_selection(field_4, nullptr);
g_object_unref(field_1);
g_object_unref(field_2);
g_object_unref(field_3);
g_object_unref(field_4);
g_object_unref(container);
}
// Adapt a GCallback that sets a flag to a base::RepeatingCallback.
base::RepeatingCallback<void(AtkObject*, gint)> AdaptGCallbackToSetFlag(
bool* flag) {
return base::BindRepeating(
[](bool* flag, AtkObject* object, gint offset) {
// Set the flag to true. This mimics the original GCallback's behavior.
*flag = true;
},
base::Unretained(flag));
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestTextEventsInStaticText) {
LoadInitialAccessibilityTreeFromHtml(std::string(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div contenteditable="true">Text inside field</div>
anonymous block
</body>
</html>)HTML"));
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(2, atk_object_get_n_accessible_children(document));
AtkObject* div_element = atk_object_ref_accessible_child(document, 0);
EXPECT_EQ(1, atk_object_get_n_accessible_children(ATK_OBJECT(div_element)));
AtkObject* text = atk_object_ref_accessible_child(ATK_OBJECT(div_element), 0);
AtkObject* anonymous_block = atk_object_ref_accessible_child(document, 1);
bool saw_caret_move_in_text = false;
ScopedGSignal caret_move_in_text_signal(
text, "text-caret-moved",
AdaptGCallbackToSetFlag(&saw_caret_move_in_text));
bool saw_caret_move_in_div = false;
ScopedGSignal caret_move_in_div_signal(
div_element, "text-caret-moved",
AdaptGCallbackToSetFlag(&saw_caret_move_in_div));
bool saw_caret_move_in_anonymous_block = false;
ScopedGSignal caret_move_in_anonymous_block_signal(
anonymous_block, "text-caret-moved",
AdaptGCallbackToSetFlag(&saw_caret_move_in_anonymous_block));
bool saw_caret_move_in_document = false;
ScopedGSignal caret_move_in_document_signal(
document, "text-caret-moved",
AdaptGCallbackToSetFlag(&saw_caret_move_in_document));
AccessibilityNotificationWaiter selection_waiter(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
ExecuteScript(
u"let selection = document.getSelection();"
u"let editable = document.querySelector('div[contenteditable=\"true\"]');"
u"editable.focus();"
u"let range = document.createRange();"
u"range.setStart(editable.lastChild, 4);"
u"range.setEnd(editable.lastChild, 4);"
u"selection.removeAllRanges();"
u"selection.addRange(range);");
ASSERT_TRUE(selection_waiter.WaitForNotification());
// We should see the event happen in div and not the static text element.
EXPECT_TRUE(saw_caret_move_in_div);
EXPECT_FALSE(saw_caret_move_in_text);
EXPECT_FALSE(saw_caret_move_in_anonymous_block);
EXPECT_FALSE(saw_caret_move_in_document);
saw_caret_move_in_div = false;
AccessibilityNotificationWaiter document_selection_waiter(
shell()->web_contents(), ax::mojom::Event::kDocumentSelectionChanged);
atk_text_set_caret_offset(ATK_TEXT(anonymous_block), 3);
ASSERT_TRUE(document_selection_waiter.WaitForNotification());
EXPECT_FALSE(saw_caret_move_in_div);
EXPECT_FALSE(saw_caret_move_in_text);
EXPECT_FALSE(saw_caret_move_in_anonymous_block);
EXPECT_TRUE(saw_caret_move_in_document);
g_object_unref(div_element);
g_object_unref(anonymous_block);
g_object_unref(text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TextAttributesInInputWithAriaHidden) {
LoadInitialAccessibilityTreeFromHtml(std::string(
R"HTML(<!DOCTYPE html>
<html>
<body>
<input aria-hidden="true" autofocus>
</body>
</html>)HTML"));
AtkObject* document = GetRendererAccessible();
EXPECT_EQ(1, atk_object_get_n_accessible_children(document));
AtkObject* section = atk_object_ref_accessible_child(document, 0);
AtkText* input_element =
ATK_TEXT(atk_object_ref_accessible_child(section, 0));
AtkAttributeSet* attributes =
atk_text_get_run_attributes(input_element, 0, nullptr, nullptr);
ASSERT_NE(attributes, nullptr);
atk_attribute_set_free(attributes);
g_object_unref(input_element);
g_object_unref(section);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestFindInPageEvents) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<!DOCTYPE html>
<html>
<body>
<div contenteditable="true">
Sufficiently long div content
</div>
<div contenteditable="true">
Second sufficiently long div content
</div>
</body>
</html>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
AtkObject* div1 = atk_object_ref_accessible_child(document, 0);
AtkObject* div2 = atk_object_ref_accessible_child(document, 1);
EXPECT_NE(div1, nullptr);
EXPECT_NE(div2, nullptr);
auto selection_callback =
G_CALLBACK(+[](AtkText*, int* count) { *count += 1; });
int selection_changed_signals = 0;
g_signal_connect(div1, "text-selection-changed", selection_callback,
&selection_changed_signals);
g_signal_connect(div2, "text-selection-changed", selection_callback,
&selection_changed_signals);
auto caret_callback = G_CALLBACK(
+[](AtkText*, int new_position, int* caret_position_from_event) {
*caret_position_from_event = new_position;
});
int caret_position_from_event = -1;
g_signal_connect(div1, "text-caret-moved", caret_callback,
&caret_position_from_event);
g_signal_connect(div2, "text-caret-moved", caret_callback,
&caret_position_from_event);
AccessibilityNotificationWaiter waiter(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
atk_text_set_caret_offset(ATK_TEXT(div1), 4);
ASSERT_TRUE(waiter.WaitForNotification());
ASSERT_EQ(atk_text_get_caret_offset(ATK_TEXT(div1)), 4);
ASSERT_EQ(caret_position_from_event, 4);
ASSERT_EQ(selection_changed_signals, 0);
caret_position_from_event = -1;
selection_changed_signals = 0;
auto* node = static_cast<ui::AXPlatformNodeAuraLinux*>(
ui::AXPlatformNode::FromNativeViewAccessible(div2));
node->ActivateFindInPageResult(1, 3);
ASSERT_EQ(selection_changed_signals, 1);
ASSERT_EQ(caret_position_from_event, 3);
ASSERT_EQ(atk_text_get_caret_offset(ATK_TEXT(div2)), 3);
ASSERT_EQ(atk_text_get_caret_offset(ATK_TEXT(div1)), 4);
caret_position_from_event = -1;
selection_changed_signals = 0;
node->TerminateFindInPage();
ASSERT_EQ(selection_changed_signals, 0);
ASSERT_EQ(caret_position_from_event, -1);
ASSERT_EQ(atk_text_get_caret_offset(ATK_TEXT(div2)), -1);
ASSERT_EQ(atk_text_get_caret_offset(ATK_TEXT(div1)), 4);
g_object_unref(div1);
g_object_unref(div2);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestOffsetsOfSelectionAll) {
LoadInitialAccessibilityTreeFromHtml(R"HTML(
<p>Hello world.</p>
<p>Another paragraph.</p>
<p>Goodbye world.</p>
<script>
var root = document.documentElement;
window.getSelection().selectAllChildren(root);
</script>)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
{
auto* node = static_cast<ui::AXPlatformNodeAuraLinux*>(
ui::AXPlatformNode::FromNativeViewAccessible(document));
std::pair<int, int> offsets = node->GetSelectionOffsetsForAtk();
EXPECT_EQ(0, offsets.first);
EXPECT_EQ(3, offsets.second);
}
std::vector<int> expected = {12, 18, 14}; // text length of each child
int number_of_children = atk_object_get_n_accessible_children(document);
for (int i = 0; i < number_of_children; i++) {
AtkObject* p = atk_object_ref_accessible_child(document, i);
EXPECT_NE(p, nullptr);
auto* node = static_cast<ui::AXPlatformNodeAuraLinux*>(
ui::AXPlatformNode::FromNativeViewAccessible(p));
std::pair<int, int> offsets = node->GetSelectionOffsetsForAtk();
EXPECT_EQ(0, offsets.first);
EXPECT_EQ(expected[i], offsets.second);
g_object_unref(p);
}
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
UniqueIdIsStableAfterRoleChange) {
LoadInitialAccessibilityTreeFromHtml("<h1>Hello</h1>");
AtkObject* document = GetRendererAccessible();
AtkObject* atk_heading = atk_object_ref_accessible_child(document, 0);
auto* heading = static_cast<ui::AXPlatformNodeBase*>(
ui::AXPlatformNode::FromNativeViewAccessible(atk_heading));
EXPECT_EQ(heading->GetRole(), ax::mojom::Role::kHeading);
int32_t heading_unique_id = heading->GetUniqueId();
EXPECT_GT(heading_unique_id, 0);
// Change the heading to a group. This will cause it to get a new AXObject on
// the renderer side, but the id will remain the same.
AccessibilityNotificationWaiter waiter(
shell()->web_contents(), ui::AXEventGenerator::Event::ROLE_CHANGED);
ExecuteScript(u"document.querySelector('h1').setAttribute('role', 'group');");
ASSERT_TRUE(waiter.WaitForNotification());
AtkObject* atk_group = atk_object_ref_accessible_child(document, 0);
auto* group = static_cast<ui::AXPlatformNodeBase*>(
ui::AXPlatformNode::FromNativeViewAccessible(atk_group));
EXPECT_EQ(group->GetRole(), ax::mojom::Role::kGroup);
int32_t group_unique_id = group->GetUniqueId();
EXPECT_GT(group_unique_id, 0);
// The incoming id from the renderer remains the same.
ASSERT_EQ(heading->GetNodeId(), group->GetNodeId());
// The outgoing id assigned on the browser side, which is unique within the
// window, also remains the same.
ASSERT_EQ(heading_unique_id, group_unique_id);
g_object_unref(atk_heading);
g_object_unref(atk_group);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
UniqueIdIsStableAfterLayoutObjectReplacement) {
LoadInitialAccessibilityTreeFromHtml(
"<main style='display:block'>Hello</main>");
AtkObject* document = GetRendererAccessible();
AtkObject* atk_block = atk_object_ref_accessible_child(document, 0);
auto* block = static_cast<ui::AXPlatformNodeBase*>(
ui::AXPlatformNode::FromNativeViewAccessible(atk_block));
EXPECT_EQ(block->GetRole(), ax::mojom::Role::kMain);
int32_t block_unique_id = block->GetUniqueId();
EXPECT_GT(block_unique_id, 0);
// Change the block to a inline_block. This will cause it to get a new
// AXObject on the renderer side, but the id will remain the same.
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ax::mojom::Event::kEndOfTest);
ExecuteScript(
u"document.querySelector('main').style.display = 'inline-block';");
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ui::BrowserAccessibilityManager* manager =
web_contents->GetRootBrowserAccessibilityManager();
manager->SignalEndOfTest();
ASSERT_TRUE(waiter.WaitForNotification());
AtkObject* atk_inline_block = atk_object_ref_accessible_child(document, 0);
auto* inline_block = static_cast<ui::AXPlatformNodeBase*>(
ui::AXPlatformNode::FromNativeViewAccessible(atk_inline_block));
EXPECT_EQ(inline_block->GetRole(), ax::mojom::Role::kMain);
int32_t inline_block_unique_id = inline_block->GetUniqueId();
EXPECT_GT(inline_block_unique_id, 0);
// The incoming id from the renderer remains the same.
ASSERT_EQ(block->GetNodeId(), inline_block->GetNodeId());
// The outgoing id assigned on the browser side, which is unique within the
// window, also remains the same.
ASSERT_EQ(block_unique_id, inline_block_unique_id);
g_object_unref(atk_block);
g_object_unref(atk_inline_block);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestGetIndexInParent) {
LoadInitialAccessibilityTreeFromHtml(R"HTML(
<p>Hello world</p>
<p>Another paragraph.</p>
<p>Goodbye world.</p>
)HTML");
// Retrieve the AtkObject interface for the document node.
AtkObject* document = GetRendererAccessible();
ASSERT_TRUE(ATK_IS_COMPONENT(document));
EXPECT_EQ(0, atk_object_get_index_in_parent(document));
int number_of_children = atk_object_get_n_accessible_children(document);
for (int i = 0; i < number_of_children; i++) {
AtkObject* p = atk_object_ref_accessible_child(document, i);
EXPECT_NE(p, nullptr);
EXPECT_EQ(i, atk_object_get_index_in_parent(p));
g_object_unref(p);
}
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
HitTestOnAncestorOfWebRoot) {
// Load the page.
LoadInitialAccessibilityTreeFromHtml(R"HTML(
<button>This is a button</button>
)HTML");
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
ui::BrowserAccessibilityManager* manager =
web_contents->GetRootBrowserAccessibilityManager();
// Find a node to hit test. Note that this is a really simple page,
// so synchronous hit testing will work fine.
ui::BrowserAccessibility* node = manager->GetBrowserAccessibilityRoot();
while (node && node->GetRole() != ax::mojom::Role::kButton) {
node = manager->NextInTreeOrder(node);
}
DCHECK(node);
// Get the screen bounds of the hit target and find the point in the middle.
gfx::Rect bounds = node->GetClippedScreenBoundsRect();
gfx::Point point = bounds.CenterPoint();
// Get the root AXPlatformNodeAuraLinux.
ui::AXPlatformNodeAuraLinux* root_platform_node =
static_cast<ui::AXPlatformNodeAuraLinux*>(
ui::AXPlatformNode::FromNativeViewAccessible(
manager->GetBrowserAccessibilityRoot()
->GetNativeViewAccessible()));
// First test that calling accHitTest on the root node returns the button.
{
gfx::NativeViewAccessible hit_child = root_platform_node->HitTestSync(
point.x(), point.y(), AtkCoordType::ATK_XY_SCREEN);
ASSERT_NE(nullptr, hit_child);
ui::AXPlatformNode* hit_child_node =
ui::AXPlatformNode::FromNativeViewAccessible(hit_child);
ASSERT_NE(nullptr, hit_child_node);
EXPECT_EQ(node->GetId(), hit_child_node->GetDelegate()->GetData().id);
}
// Now test it again, but this time caliing accHitTest on the parent
// IAccessible of the web root node.
{
RenderWidgetHostViewAura* rwhva = static_cast<RenderWidgetHostViewAura*>(
shell()->web_contents()->GetRenderWidgetHostView());
gfx::NativeViewAccessible ancestor = rwhva->GetParentNativeViewAccessible();
ASSERT_NE(nullptr, ancestor);
ui::AXPlatformNodeAuraLinux* ancestor_node =
static_cast<ui::AXPlatformNodeAuraLinux*>(
ui::AXPlatformNode::FromNativeViewAccessible(ancestor));
ASSERT_NE(nullptr, ancestor_node);
gfx::NativeViewAccessible hit_child = ancestor_node->HitTestSync(
point.x(), point.y(), AtkCoordType::ATK_XY_SCREEN);
ASSERT_NE(nullptr, hit_child);
ui::AXPlatformNode* hit_child_node =
ui::AXPlatformNode::FromNativeViewAccessible(hit_child);
ASSERT_NE(nullptr, hit_child_node);
EXPECT_EQ(node->GetId(), hit_child_node->GetDelegate()->GetData().id);
}
}
// Tests if it does not DCHECK when textarea has a placeholder break element.
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestGetTextContainerFromTextArea) {
std::string content = std::string("<textarea style=\"height:100px;\">hello") +
std::string("\n") + std::string("</textarea>");
LoadInitialAccessibilityTreeFromHtml(content);
AtkText* atk_text = FindNode(ATK_ROLE_ENTRY);
AtkTextRectangle atk_rect;
// atk_text_get_range_extents() calls GetTextContainerForPlainTextField() and
// DCHECK on checking children counts.
atk_text_get_range_extents(atk_text, 0, 7, AtkCoordType::ATK_XY_SCREEN,
&atk_rect);
g_object_unref(atk_text);
}
IN_PROC_BROWSER_TEST_F(AccessibilityAuraLinuxBrowserTest,
TestCaretMovedInNumberInput) {
LoadInitialAccessibilityTreeFromHtml(
R"HTML(<input type="number" value="12">
)HTML");
AccessibilityNotificationWaiter waiter(
shell()->web_contents(),
ui::AXEventGenerator::Event::TEXT_SELECTION_CHANGED);
auto caret_callback =
G_CALLBACK(+[](AtkText*, int new_position, int* out_caret_position) {
*out_caret_position = new_position;
});
int out_caret_position = -1;
AtkText* input_text = FindNode(ATK_ROLE_SPIN_BUTTON);
g_signal_connect(input_text, "text-caret-moved", caret_callback,
&out_caret_position);
atk_text_set_caret_offset(input_text, 0);
ASSERT_TRUE(waiter.WaitForNotification());
EXPECT_EQ(atk_text_get_caret_offset(input_text), 0);
EXPECT_EQ(out_caret_position, 0);
atk_text_set_caret_offset(input_text, 1);
ASSERT_TRUE(waiter.WaitForNotification());
EXPECT_EQ(atk_text_get_caret_offset(input_text), 1);
EXPECT_EQ(out_caret_position, 1);
atk_text_set_caret_offset(input_text, 2);
ASSERT_TRUE(waiter.WaitForNotification());
EXPECT_EQ(atk_text_get_caret_offset(input_text), 2);
EXPECT_EQ(out_caret_position, 2);
g_object_unref(input_text);
}
} // namespace content