blob: 99134efa64d0539cde2c1401ca86b1e140fe10ee [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
#include <string>
#include "base/logging.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_focus_options.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/shadow_root.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/selection_modifier.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/set_selection_options.h"
#include "third_party/blink/renderer/core/editing/text_affinity.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/html/html_br_element.h"
#include "third_party/blink/renderer/core/html/html_div_element.h"
#include "third_party/blink/renderer/core/html/html_paragraph_element.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/modules/accessibility/testing/accessibility_selection_test.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
namespace blink {
namespace test {
//
// Basic tests.
//
TEST_F(AccessibilitySelectionTest, FromCurrentSelection) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<p id="paragraph1">Hello.</p>
<p id="paragraph2">How are you?</p>
)HTML");
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let text1 = document.querySelectorAll('p')[0].firstChild;
let paragraph2 = document.querySelectorAll('p')[1];
let range = document.createRange();
range.setStart(text1, 3);
range.setEnd(paragraph2, 1);
let selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
const AXObject* ax_static_text_1 =
GetAXObjectByElementId("paragraph1")->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text_1);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text_1->RoleValue());
const AXObject* ax_paragraph_2 = GetAXObjectByElementId("paragraph2");
ASSERT_NE(nullptr, ax_paragraph_2);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph_2->RoleValue());
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_static_text_1, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(3, ax_selection.Anchor().TextOffset());
ASSERT_FALSE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_paragraph_2, ax_selection.Focus().ContainerObject());
EXPECT_EQ(1, ax_selection.Focus().ChildIndex());
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hel^lo.>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: How are you?>\n|",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, FromCurrentSelectionSelectAll) {
SetBodyInnerHTML(R"HTML(
<p id="paragraph1">Hello.</p>
<p id="paragraph2">How are you?</p>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Selection().SelectAll(SetSelectionBy::kUser);
UpdateAllLifecyclePhasesForTest();
ASSERT_NE(nullptr, GetAXRootObject());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_FALSE(ax_selection.Anchor().IsTextPosition());
AXObject* html_object = GetAXRootObject()->ChildAtIncludingIgnored(0);
ASSERT_NE(nullptr, html_object);
EXPECT_EQ(html_object, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection.Anchor().ChildIndex());
ASSERT_FALSE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(html_object, ax_selection.Focus().ContainerObject());
EXPECT_EQ(html_object->ChildCountIncludingIgnored(),
ax_selection.Focus().ChildIndex());
EXPECT_EQ(
"++<GenericContainer>\n"
"^++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hello.>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: How are you?>\n|",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, ClearCurrentSelection) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<p>Hello.</p>
<p>How are you?</p>
)HTML");
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let text1 = document.querySelectorAll('p')[0].firstChild;
let paragraph2 = document.querySelectorAll('p')[1];
let range = document.createRange();
range.setStart(text1, 3);
range.setEnd(paragraph2, 1);
let selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
SelectionInDOMTree selection = Selection().GetSelectionInDOMTree();
ASSERT_FALSE(selection.IsNone());
AXSelection::ClearCurrentSelection(GetDocument());
selection = Selection().GetSelectionInDOMTree();
EXPECT_TRUE(selection.IsNone());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
EXPECT_FALSE(ax_selection.IsValid());
EXPECT_EQ("", GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, CancelSelect) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<p id="paragraph1">Hello.</p>
<p id="paragraph2">How are you?</p>
)HTML");
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
document.addEventListener("selectstart", (e) => {
e.preventDefault();
}, false);
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const AXObject* ax_static_text_1 =
GetAXObjectByElementId("paragraph1")->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text_1);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text_1->RoleValue());
const AXObject* ax_paragraph_2 = GetAXObjectByElementId("paragraph2");
ASSERT_NE(nullptr, ax_paragraph_2);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph_2->RoleValue());
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(
AXPosition::CreatePositionInTextObject(*ax_static_text_1, 3))
.SetFocus(AXPosition::CreateLastPositionInObject(*ax_paragraph_2))
.Build();
EXPECT_FALSE(ax_selection.Select()) << "The operation has been cancelled.";
EXPECT_TRUE(Selection().GetSelectionInDOMTree().IsNone());
EXPECT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
GetDocument().RemoveAllEventListeners();
EXPECT_TRUE(ax_selection.Select()) << "The operation should now go through.";
EXPECT_FALSE(Selection().GetSelectionInDOMTree().IsNone());
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hel^lo.>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: How are you?>\n|",
GetSelectionText(AXSelection::FromCurrentSelection(GetDocument())));
}
TEST_F(AccessibilitySelectionTest, DocumentRangeMatchesSelection) {
SetBodyInnerHTML(R"HTML(
<p id="paragraph1">Hello.</p>
<p id="paragraph2">How are you?</p>
)HTML");
const AXObject* ax_static_text_1 =
GetAXObjectByElementId("paragraph1")->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text_1);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text_1->RoleValue());
const AXObject* ax_paragraph_2 = GetAXObjectByElementId("paragraph2");
ASSERT_NE(nullptr, ax_paragraph_2);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_paragraph_2->RoleValue());
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(
AXPosition::CreatePositionInTextObject(*ax_static_text_1, 3))
.SetFocus(AXPosition::CreateLastPositionInObject(*ax_paragraph_2))
.Build();
EXPECT_TRUE(ax_selection.Select());
ASSERT_FALSE(Selection().GetSelectionInDOMTree().IsNone());
ASSERT_NE(nullptr, Selection().DocumentCachedRange());
EXPECT_EQ(String("lo.\n How are you?"),
Selection().DocumentCachedRange()->toString());
}
TEST_F(AccessibilitySelectionTest, SetSelectionInText) {
SetBodyInnerHTML(R"HTML(<p id="paragraph">Hello</p>)HTML");
const Node* text =
GetDocument().QuerySelector(AtomicString("p"))->firstChild();
ASSERT_NE(nullptr, text);
ASSERT_TRUE(text->IsTextNode());
const AXObject* ax_static_text =
GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
const auto ax_base =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto ax_extent = AXPosition::CreatePositionAfterObject(*ax_static_text);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetAnchor(ax_base).SetFocus(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(3, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Focus().AnchorNode());
EXPECT_EQ(5, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hel^lo|>\n",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, SetSelectionInMultilineTextarea) {
// On Android we use an ifdef to disable inline text boxes.
#if !BUILDFLAG(IS_ANDROID)
ui::AXMode mode(ui::kAXModeComplete);
mode.set_mode(ui::AXMode::kInlineTextBoxes, true);
ax_context_->SetAXMode(mode);
GetAXObjectCache().MarkDocumentDirty();
GetAXObjectCache().UpdateAXForAllDocuments();
LoadAhem();
SetBodyInnerHTML(R"HTML(
<textarea id="txt" style="width:80px; height:81px; font-family: Ahem; font-size: 4;">hello text go blue</textarea>
)HTML");
// This HTML generates the following ax tree:
// id#=13 rootWebArea
// ++id#=14 genericContainer
// ++++id#=15 genericContainer
// ++++++id#=16 textField
// ++++++++id#=17 genericContainer
// ++++++++++id#=18 staticText name='hello text go blue<newline>'
// ++++++++++++id#=20 inlineTextBox name='hello'
// ++++++++++++id#=22 inlineTextBox name='text'
// ++++++++++++id#=22 inlineTextBox name='go'
// ++++++++++++id#=22 inlineTextBox name='blue'
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
textarea->Focus(FocusOptions::Create());
ASSERT_TRUE(textarea->IsFocusedElementInDocument());
const AXObject* ax_textarea = GetAXObjectByElementId("txt");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
AXObject* ax_inline_text_box = ax_textarea->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_inline_text_box);
ASSERT_EQ(ax_inline_text_box->RoleValue(),
ax::mojom::Role::kGenericContainer);
ax_inline_text_box = ax_inline_text_box->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_inline_text_box);
ASSERT_EQ(ax_inline_text_box->ComputedName(), "hello text go blue");
ASSERT_EQ(ax_inline_text_box->RoleValue(), ax::mojom::Role::kStaticText);
ax_inline_text_box = ax_inline_text_box->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_inline_text_box);
ASSERT_EQ(ax_inline_text_box->ComputedName(), "hello");
ASSERT_EQ(ax_inline_text_box->RoleValue(), ax::mojom::Role::kInlineTextBox);
ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored()
->NextSiblingIncludingIgnored();
ASSERT_NE(nullptr, ax_inline_text_box);
ASSERT_EQ(ax_inline_text_box->RoleValue(), ax::mojom::Role::kInlineTextBox);
ASSERT_EQ(ax_inline_text_box->ComputedName(), "text");
ax_inline_text_box = ax_inline_text_box->NextSiblingIncludingIgnored()
->NextSiblingIncludingIgnored();
ASSERT_NE(nullptr, ax_inline_text_box);
ASSERT_EQ(ax_inline_text_box->RoleValue(), ax::mojom::Role::kInlineTextBox);
ASSERT_EQ(ax_inline_text_box->ComputedName(), "go");
const auto ax_base =
AXPosition::CreatePositionInTextObject(*ax_inline_text_box, 0);
const auto ax_extent =
AXPosition::CreatePositionInTextObject(*ax_inline_text_box, 2);
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(ax_base).SetFocus(ax_extent).Build();
EXPECT_TRUE(ax_selection.Select());
// Even though the selection is set to offsets 0,4 "text" in the inline text
// box, the selection needs to end up in offsets 12,16 on the whole textarea
// so that "text" is the selection.
EXPECT_EQ(11u, ToTextControl(*textarea).selectionStart());
EXPECT_EQ(13u, ToTextControl(*textarea).selectionEnd());
#endif // !BUILDFLAG(IS_ANDROID)
}
TEST_F(AccessibilitySelectionTest, SetSelectionInTextWithWhiteSpace) {
SetBodyInnerHTML(R"HTML(<p id="paragraph"> Hello</p>)HTML");
const Node* text =
GetDocument().QuerySelector(AtomicString("p"))->firstChild();
ASSERT_NE(nullptr, text);
ASSERT_TRUE(text->IsTextNode());
const AXObject* ax_static_text =
GetAXObjectByElementId("paragraph")->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
const auto ax_base =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto ax_extent = AXPosition::CreatePositionAfterObject(*ax_static_text);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetAnchor(ax_base).SetFocus(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(8, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Focus().AnchorNode());
EXPECT_EQ(10, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hel^lo|>\n",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, SetSelectionAcrossLineBreak) {
SetBodyInnerHTML(R"HTML(
<p id="paragraph">Hello<br id="br">How are you.</p>
)HTML");
const Node* paragraph = GetDocument().QuerySelector(AtomicString("p"));
ASSERT_NE(nullptr, paragraph);
ASSERT_TRUE(IsA<HTMLParagraphElement>(paragraph));
const Node* br = GetDocument().QuerySelector(AtomicString("br"));
ASSERT_NE(nullptr, br);
ASSERT_TRUE(IsA<HTMLBRElement>(br));
const Node* line2 =
GetDocument().QuerySelector(AtomicString("p"))->lastChild();
ASSERT_NE(nullptr, line2);
ASSERT_TRUE(line2->IsTextNode());
const AXObject* ax_br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, ax_br);
ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
const AXObject* ax_line2 =
GetAXObjectByElementId("paragraph")->LastChildIncludingIgnored();
ASSERT_NE(nullptr, ax_line2);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_line2->RoleValue());
const auto ax_base = AXPosition::CreatePositionBeforeObject(*ax_br);
const auto ax_extent = AXPosition::CreatePositionInTextObject(*ax_line2, 0);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetAnchor(ax_base).SetFocus(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(paragraph, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(line2, dom_selection.Focus().AnchorNode());
EXPECT_EQ(0, dom_selection.Focus().OffsetInContainerNode());
// The selection anchor marker '^' should be before the line break and the
// selection focus marker '|' should be after it.
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hello>\n"
"^++++++++<LineBreak: \n>\n"
"|++++++++<StaticText: |How are you.>\n",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, SetSelectionAcrossLineBreakInEditableText) {
SetBodyInnerHTML(R"HTML(
<p contenteditable id="paragraph">Hello<br id="br">How are you.</p>
)HTML");
const Node* paragraph = GetDocument().QuerySelector(AtomicString("p"));
ASSERT_NE(nullptr, paragraph);
ASSERT_TRUE(IsA<HTMLParagraphElement>(paragraph));
const Node* br = GetDocument().QuerySelector(AtomicString("br"));
ASSERT_NE(nullptr, br);
ASSERT_TRUE(IsA<HTMLBRElement>(br));
const Node* line2 =
GetDocument().QuerySelector(AtomicString("p"))->lastChild();
ASSERT_NE(nullptr, line2);
ASSERT_TRUE(line2->IsTextNode());
const AXObject* ax_br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, ax_br);
ASSERT_EQ(ax::mojom::Role::kLineBreak, ax_br->RoleValue());
const AXObject* ax_line2 =
GetAXObjectByElementId("paragraph")->LastChildIncludingIgnored();
ASSERT_NE(nullptr, ax_line2);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_line2->RoleValue());
const auto ax_base = AXPosition::CreatePositionBeforeObject(*ax_br);
// In the case of text objects, the deep equivalent position should always be
// returned, i.e. a text position before the first character.
const auto ax_extent = AXPosition::CreatePositionBeforeObject(*ax_line2);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetAnchor(ax_base).SetFocus(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(paragraph, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(line2, dom_selection.Focus().AnchorNode());
EXPECT_EQ(0, dom_selection.Focus().OffsetInContainerNode());
// The selection anchor marker '^' should be before the line break and the
// selection focus marker '|' should be after it.
EXPECT_EQ(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Paragraph>\n"
"++++++++<StaticText: Hello>\n"
"^++++++++<LineBreak: \n>\n"
"|++++++++<StaticText: |How are you.>\n",
GetSelectionText(ax_selection));
}
//
// Get selection tests.
// Retrieving a selection with endpoints which have corresponding ignored
// objects in the accessibility tree, e.g. which are display:none, should shrink
// or extend the |AXSelection| to valid endpoints.
// Note: aria-describedby adds hidden target subtrees to the a11y tree as
// "ignored but included in tree".
//
TEST_F(AccessibilitySelectionTest, SetSelectionInDisplayNone) {
SetBodyInnerHTML(R"HTML(
<div id="main" role="main" aria-describedby="hidden1 hidden2">
<p id="beforeHidden">Before display:none.</p>
<p id="hidden1" style="display:none">Display:none 1.</p>
<p id="betweenHidden">In between two display:none elements.</p>
<p id="hidden2" style="display:none">Display:none 2.</p>
<p id="afterHidden">After display:none.</p>
</div>
)HTML");
const Node* hidden_1 = GetElementById("hidden1");
ASSERT_NE(nullptr, hidden_1);
const Node* hidden_2 = GetElementById("hidden2");
ASSERT_NE(nullptr, hidden_2);
const AXObject* ax_main = GetAXObjectByElementId("main");
ASSERT_NE(nullptr, ax_main);
ASSERT_EQ(ax::mojom::Role::kMain, ax_main->RoleValue());
const AXObject* ax_before = GetAXObjectByElementId("beforeHidden");
ASSERT_NE(nullptr, ax_before);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
const AXObject* ax_hidden1 = GetAXObjectByElementId("hidden1");
ASSERT_NE(nullptr, ax_hidden1);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_hidden1->RoleValue());
ASSERT_TRUE(ax_hidden1->AccessibilityIsIgnored());
ASSERT_TRUE(ax_hidden1->AccessibilityIsIncludedInTree());
const AXObject* ax_hidden1_text = ax_hidden1->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_hidden1_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_hidden1_text->RoleValue());
ASSERT_TRUE(ax_hidden1_text->AccessibilityIsIgnored());
ASSERT_TRUE(ax_hidden1_text->AccessibilityIsIncludedInTree());
const AXObject* ax_between = GetAXObjectByElementId("betweenHidden");
ASSERT_NE(nullptr, ax_between);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_between->RoleValue());
const AXObject* ax_hidden2 = GetAXObjectByElementId("hidden2");
ASSERT_NE(nullptr, ax_hidden2);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_hidden2->RoleValue());
ASSERT_TRUE(ax_hidden2->AccessibilityIsIgnored());
ASSERT_TRUE(ax_hidden2->AccessibilityIsIncludedInTree());
const AXObject* ax_hidden2_text = ax_hidden2->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_hidden2_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_hidden2_text->RoleValue());
ASSERT_TRUE(ax_hidden2_text->AccessibilityIsIgnored());
ASSERT_TRUE(ax_hidden2_text->AccessibilityIsIncludedInTree());
const AXObject* ax_after = GetAXObjectByElementId("afterHidden");
ASSERT_NE(nullptr, ax_after);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
const auto hidden_1_first = Position::FirstPositionInNode(*hidden_1);
const auto hidden_2_first = Position::FirstPositionInNode(*hidden_2);
const auto selection = SelectionInDOMTree::Builder()
.SetBaseAndExtent(hidden_1_first, hidden_2_first)
.Build();
const auto ax_selection_shrink = AXSelection::FromSelection(
selection, AXSelectionBehavior::kShrinkToValidRange);
const auto ax_selection_extend = AXSelection::FromSelection(
selection, AXSelectionBehavior::kExtendToValidRange);
// The "display: none" content is included in the AXTree as an ignored node,
// so shrunk selection should include those AXObjects. The tree in the browser
// process also includes those ignored nodes, and the position will be
// adjusted according to AXPosition rules; in particular, a position anchored
// before a text node is explicitly moved to before the first character of the
// text object.
ASSERT_TRUE(ax_selection_shrink.Anchor().IsTextPosition());
EXPECT_EQ(ax_hidden1_text, ax_selection_shrink.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection_shrink.Anchor().TextOffset());
ASSERT_TRUE(ax_selection_shrink.Focus().IsTextPosition());
EXPECT_EQ(ax_hidden2_text, ax_selection_shrink.Focus().ContainerObject());
EXPECT_EQ(0, ax_selection_shrink.Focus().TextOffset());
// The extended selection should start in the "display: none" content because
// they are included in the AXTree. Similarly to above, the position will be
// adjusted to point to the first character of the text object.
ASSERT_TRUE(ax_selection_extend.Anchor().IsTextPosition());
EXPECT_EQ(ax_hidden1_text, ax_selection_extend.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection_extend.Anchor().TextOffset());
ASSERT_TRUE(ax_selection_extend.Focus().IsTextPosition());
EXPECT_EQ(ax_hidden2_text, ax_selection_extend.Focus().ContainerObject());
EXPECT_EQ(0, ax_selection_extend.Focus().TextOffset());
// Even though the two AX selections have different anchors and foci, the text
// selected in the accessibility tree should not differ, because any
// differences in the equivalent DOM selections concern elements that are
// display:none. However, the AX selections should still differ if converted
// to DOM selections.
const std::string selection_text(
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Main>\n"
"++++++++<Paragraph>\n"
"++++++++++<StaticText: Before display:none.>\n"
"++++++++<Paragraph>\n"
"^++++++++++<StaticText: ^Display:none 1.>\n"
"++++++++<Paragraph>\n"
"++++++++++<StaticText: In between two display:none elements.>\n"
"++++++++<Paragraph>\n"
"|++++++++++<StaticText: |Display:none 2.>\n"
"++++++++<Paragraph>\n"
"++++++++++<StaticText: After display:none.>\n");
EXPECT_EQ(selection_text, GetSelectionText(ax_selection_shrink));
EXPECT_EQ(selection_text, GetSelectionText(ax_selection_extend));
}
//
// Set selection tests.
// Setting the selection from an |AXSelection| that has endpoints which are not
// present in the layout tree should shrink or extend the selection to visible
// endpoints.
//
TEST_F(AccessibilitySelectionTest, SetSelectionAroundListBullet) {
SetBodyInnerHTML(R"HTML(
<div role="main">
<ul>
<li id="item1">Item 1.</li>
<li id="item2">Item 2.</li>
</ul>
</div>
)HTML");
const Node* item_1 = GetElementById("item1");
ASSERT_NE(nullptr, item_1);
ASSERT_FALSE(item_1->IsTextNode());
const Node* text_1 = item_1->firstChild();
ASSERT_NE(nullptr, text_1);
ASSERT_TRUE(text_1->IsTextNode());
const Node* item_2 = GetElementById("item2");
ASSERT_NE(nullptr, item_2);
ASSERT_FALSE(item_2->IsTextNode());
const Node* text_2 = item_2->firstChild();
ASSERT_NE(nullptr, text_2);
ASSERT_TRUE(text_2->IsTextNode());
const AXObject* ax_item_1 = GetAXObjectByElementId("item1");
ASSERT_NE(nullptr, ax_item_1);
ASSERT_EQ(ax::mojom::Role::kListItem, ax_item_1->RoleValue());
const AXObject* ax_bullet_1 = ax_item_1->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_bullet_1);
ASSERT_EQ(ax::mojom::Role::kListMarker, ax_bullet_1->RoleValue());
const AXObject* ax_item_2 = GetAXObjectByElementId("item2");
ASSERT_NE(nullptr, ax_item_2);
ASSERT_EQ(ax::mojom::Role::kListItem, ax_item_2->RoleValue());
const AXObject* ax_text_2 = ax_item_2->LastChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text_2);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_2->RoleValue());
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreateFirstPositionInObject(*ax_bullet_1))
.SetFocus(AXPosition::CreateLastPositionInObject(*ax_text_2))
.Build();
// The list bullet is not included in the DOM tree. Shrinking the
// |AXSelection| should skip over it by creating an anchor before the first
// child of the first <li>, i.e. the text node containing the text "Item 1.".
// This should be further optimized to a text position at the start of the
// text object inside the first <li>.
ax_selection.Select(AXSelectionBehavior::kShrinkToValidRange);
const SelectionInDOMTree shrunk_selection =
Selection().GetSelectionInDOMTree();
EXPECT_EQ(text_1, shrunk_selection.Anchor().AnchorNode());
ASSERT_TRUE(shrunk_selection.Anchor().IsOffsetInAnchor());
EXPECT_EQ(0, shrunk_selection.Anchor().OffsetInContainerNode());
ASSERT_TRUE(shrunk_selection.Focus().IsOffsetInAnchor());
EXPECT_EQ(text_2, shrunk_selection.Focus().AnchorNode());
EXPECT_EQ(7, shrunk_selection.Focus().OffsetInContainerNode());
// The list bullet is not included in the DOM tree. Extending the
// |AXSelection| should move the anchor to before the first <li>.
ax_selection.Select(AXSelectionBehavior::kExtendToValidRange);
const SelectionInDOMTree extended_selection =
Selection().GetSelectionInDOMTree();
ASSERT_TRUE(extended_selection.Anchor().IsOffsetInAnchor());
EXPECT_EQ(item_1->parentNode(), extended_selection.Anchor().AnchorNode());
EXPECT_EQ(static_cast<int>(item_1->NodeIndex()),
extended_selection.Anchor().OffsetInContainerNode());
ASSERT_TRUE(extended_selection.Focus().IsOffsetInAnchor());
EXPECT_EQ(text_2, extended_selection.Focus().AnchorNode());
EXPECT_EQ(7, extended_selection.Focus().OffsetInContainerNode());
std::string expectations;
expectations =
"++<GenericContainer>\n"
"++++<GenericContainer>\n"
"++++++<Main>\n"
"++++++++<List>\n"
"++++++++++<ListItem>\n"
"++++++++++++<ListMarker: \xE2\x80\xA2 >\n"
"^++++++++++++++<StaticText: ^\xE2\x80\xA2 >\n"
"++++++++++++<StaticText: Item 1.>\n"
"++++++++++<ListItem>\n"
"++++++++++++<ListMarker: \xE2\x80\xA2 >\n"
"++++++++++++++<StaticText: \xE2\x80\xA2 >\n"
"++++++++++++<StaticText: Item 2.|>\n";
// The |AXSelection| should remain unaffected by any shrinking and should
// include both list bullets.
EXPECT_EQ(expectations, GetSelectionText(ax_selection));
}
//
// Tests that involve selection inside, outside, and spanning text controls.
//
TEST_F(AccessibilitySelectionTest, FromCurrentSelectionInTextField) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let input = document.querySelector('input');
input.focus();
input.selectionStart = 0;
input.selectionEnd = input.value.length;
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const Element* input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
const auto ax_selection =
AXSelection::FromCurrentSelection(ToTextControl(*input));
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_input, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection.Anchor().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Anchor().Affinity());
ASSERT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_input, ax_selection.Focus().ContainerObject());
EXPECT_EQ(18, ax_selection.Focus().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Focus().Affinity());
}
TEST_F(AccessibilitySelectionTest, FromCurrentSelectionInTextarea) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let textarea = document.querySelector('textarea');
textarea.focus();
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.textLength;
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const Element* textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
const auto ax_selection =
AXSelection::FromCurrentSelection(ToTextControl(*textarea));
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection.Anchor().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Anchor().Affinity());
ASSERT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Focus().ContainerObject());
EXPECT_EQ(53, ax_selection.Focus().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Focus().Affinity());
}
TEST_F(AccessibilitySelectionTest, FromCurrentSelectionInTextareaWithAffinity) {
// Even though the base of the selection in this test is at a position with an
// upstream affinity, only a downstream affinity should be exposed, because an
// upstream affinity is currently exposed in core editing only when the
// selection is caret.
SetBodyInnerHTML(R"HTML(
<textarea id="textarea"
rows="2" cols="15"
style="font-family: monospace; width: 15ch;">
InsideTextareaField.
</textarea>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
const TextControlElement& text_control = ToTextControl(*textarea);
// This test should only be testing accessibility code. Ordinarily we should
// be setting up the test using Javascript in order to avoid depending on the
// internal implementation of DOM selection. However, the only way I found to
// get an upstream affinity is to send the "end" key which might be unreliable
// on certain platforms, so we modify the selection using Blink internal
// functions instead.
textarea->Focus();
Selection().Modify(SelectionModifyAlteration::kMove,
SelectionModifyDirection::kBackward,
TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
Selection().Modify(SelectionModifyAlteration::kExtend,
SelectionModifyDirection::kForward,
TextGranularity::kLineBoundary, SetSelectionBy::kUser);
UpdateAllLifecyclePhasesForTest();
ASSERT_EQ(TextAffinity::kDownstream, text_control.Selection().Affinity());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
const auto ax_selection = AXSelection::FromCurrentSelection(text_control);
ASSERT_TRUE(ax_selection.IsValid());
EXPECT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection.Anchor().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Anchor().Affinity());
EXPECT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Focus().ContainerObject());
EXPECT_EQ(8, ax_selection.Focus().TextOffset());
EXPECT_EQ(TextAffinity::kDownstream, ax_selection.Focus().Affinity());
}
TEST_F(AccessibilitySelectionTest,
FromCurrentSelectionInTextareaWithCollapsedSelectionAndAffinity) {
SetBodyInnerHTML(R"HTML(
<textarea id="textarea"
rows="2" cols="15"
style="font-family: monospace; width: 15ch;">
InsideTextareaField.
</textarea>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
const TextControlElement& text_control = ToTextControl(*textarea);
// This test should only be testing accessibility code. Ordinarily we should
// be setting up the test using Javascript in order to avoid depending on the
// internal implementation of DOM selection. However, the only way I found to
// get an upstream affinity is to send the "end" key which might be unreliable
// on certain platforms, so we modify the selection using Blink internal
// functions instead.
textarea->Focus();
Selection().Modify(SelectionModifyAlteration::kMove,
SelectionModifyDirection::kBackward,
TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
Selection().Modify(SelectionModifyAlteration::kMove,
SelectionModifyDirection::kForward,
TextGranularity::kLineBoundary, SetSelectionBy::kUser);
UpdateAllLifecyclePhasesForTest();
ASSERT_EQ(TextAffinity::kUpstream, text_control.Selection().Affinity());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
const auto ax_selection = AXSelection::FromCurrentSelection(text_control);
ASSERT_TRUE(ax_selection.IsValid());
EXPECT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(8, ax_selection.Anchor().TextOffset());
EXPECT_EQ(TextAffinity::kUpstream, ax_selection.Anchor().Affinity());
EXPECT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Focus().ContainerObject());
EXPECT_EQ(8, ax_selection.Focus().TextOffset());
EXPECT_EQ(TextAffinity::kUpstream, ax_selection.Focus().Affinity());
}
TEST_F(AccessibilitySelectionTest,
FromCurrentSelectionInContentEditableWithSoftLineBreaks) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<div id="contenteditable" role="textbox" contenteditable
style="max-width: 5px; overflow-wrap: normal;">
Inside contenteditable field.
</div>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
// We want to select all the text in the content editable, but not the
// editable itself.
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
const contenteditable = document.querySelector('div[contenteditable]');
contenteditable.focus();
const firstLine = contenteditable.firstChild;
const lastLine = contenteditable.lastChild;
const range = document.createRange();
range.setStart(firstLine, 0);
range.setEnd(lastLine, lastLine.nodeValue.length);
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const AXObject* ax_contenteditable =
GetAXObjectByElementId("contenteditable");
ASSERT_NE(nullptr, ax_contenteditable);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_contenteditable->RoleValue());
const AXObject* ax_static_text =
ax_contenteditable->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text);
// Guard against the structure of the accessibility tree unexpectedly
// changing, causing a hard to debug test failure.
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue())
<< "A content editable with only text inside it should have static text "
"children.";
// Guard against both ComputedName().length() and selection extent offset
// returning 0.
ASSERT_LT(0u, ax_static_text->ComputedName().length());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_static_text, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(0, ax_selection.Anchor().TextOffset());
ASSERT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_static_text, ax_selection.Focus().ContainerObject());
EXPECT_EQ(ax_static_text->ComputedName().length(),
static_cast<unsigned>(ax_selection.Focus().TextOffset()));
}
TEST_F(AccessibilitySelectionTest,
FromCurrentSelectionInContentEditableSelectFirstSoftLineBreak) {
GetPage().GetSettings().SetScriptEnabled(true);
// There should be no white space between the opening tag of the content
// editable and the text inside it, otherwise selection offsets would be
// wrong.
SetBodyInnerHTML(R"HTML(
<div id="contenteditable" role="textbox" contenteditable
style="max-width: 5px; overflow-wrap: normal;">Line one.
</div>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
const contenteditable = document.querySelector('div[contenteditable]');
contenteditable.focus();
const text = contenteditable.firstChild;
const range = document.createRange();
range.setStart(text, 4);
range.setEnd(text, 4);
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
selection.modify('extend', 'forward', 'character');
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const AXObject* ax_contenteditable =
GetAXObjectByElementId("contenteditable");
ASSERT_NE(nullptr, ax_contenteditable);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_contenteditable->RoleValue());
const AXObject* ax_static_text =
ax_contenteditable->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text);
// Guard against the structure of the accessibility tree unexpectedly
// changing, causing a hard to debug test failure.
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue())
<< "A content editable with only text inside it should have static text "
"children.";
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_static_text, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(4, ax_selection.Anchor().TextOffset());
ASSERT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_static_text, ax_selection.Focus().ContainerObject());
EXPECT_EQ(5, ax_selection.Focus().TextOffset());
}
TEST_F(AccessibilitySelectionTest,
FromCurrentSelectionInContentEditableSelectFirstHardLineBreak) {
GetPage().GetSettings().SetScriptEnabled(true);
// There should be no white space between the opening tag of the content
// editable and the text inside it, otherwise selection offsets would be
// wrong.
SetBodyInnerHTML(R"HTML(
<div id="contenteditable" role="textbox" contenteditable
style="max-width: 5px; overflow-wrap: normal;">Inside<br>contenteditable.
</div>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
const contenteditable = document.querySelector('div[contenteditable]');
contenteditable.focus();
const firstLine = contenteditable.firstChild;
const range = document.createRange();
range.setStart(firstLine, 6);
range.setEnd(firstLine, 6);
const selection = getSelection();
selection.removeAllRanges();
selection.addRange(range);
selection.modify('extend', 'forward', 'character');
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
const AXObject* ax_contenteditable =
GetAXObjectByElementId("contenteditable");
ASSERT_NE(nullptr, ax_contenteditable);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_contenteditable->RoleValue());
ASSERT_EQ(3, ax_contenteditable->UnignoredChildCount())
<< "The content editable should have two lines with a line break between "
"them.";
const AXObject* ax_static_text_2 = ax_contenteditable->UnignoredChildAt(2);
ASSERT_NE(nullptr, ax_static_text_2);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text_2->RoleValue());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
ASSERT_FALSE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_contenteditable, ax_selection.Anchor().ContainerObject());
EXPECT_EQ(1, ax_selection.Anchor().ChildIndex());
ASSERT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_static_text_2, ax_selection.Focus().ContainerObject());
EXPECT_EQ(0, ax_selection.Focus().TextOffset());
}
TEST_F(AccessibilitySelectionTest, ClearCurrentSelectionInTextField) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let input = document.querySelector('input');
input.focus();
input.selectionStart = 0;
input.selectionEnd = input.textLength;
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
SelectionInDOMTree selection = Selection().GetSelectionInDOMTree();
ASSERT_FALSE(selection.IsNone());
AXSelection::ClearCurrentSelection(GetDocument());
selection = Selection().GetSelectionInDOMTree();
EXPECT_TRUE(selection.IsNone());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
EXPECT_FALSE(ax_selection.IsValid());
EXPECT_EQ("", GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, ClearCurrentSelectionInTextarea) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let textarea = document.querySelector('textarea');
textarea.focus();
textarea.selectionStart = 0;
textarea.selectionEnd = textarea.textLength;
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
SelectionInDOMTree selection = Selection().GetSelectionInDOMTree();
ASSERT_FALSE(selection.IsNone());
AXSelection::ClearCurrentSelection(GetDocument());
selection = Selection().GetSelectionInDOMTree();
EXPECT_TRUE(selection.IsNone());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
EXPECT_FALSE(ax_selection.IsValid());
EXPECT_EQ("", GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, ForwardSelectionInTextField) {
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
input->Focus(FocusOptions::Create());
ASSERT_TRUE(input->IsFocusedElementInDocument());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
// Forward selection.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreateFirstPositionInObject(*ax_input))
.SetFocus(AXPosition::CreateLastPositionInObject(*ax_input))
.Build();
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(0u, ToTextControl(*input).selectionStart());
EXPECT_EQ(18u, ToTextControl(*input).selectionEnd());
EXPECT_EQ("forward", ToTextControl(*input).selectionDirection());
// Ensure that the selection that was just set could be successfully
// retrieved.
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_current_selection =
AXSelection::FromCurrentSelection(ToTextControl(*input));
EXPECT_EQ(ax_selection, ax_current_selection);
}
TEST_F(AccessibilitySelectionTest, BackwardSelectionInTextField) {
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
input->Focus(FocusOptions::Create());
ASSERT_TRUE(input->IsFocusedElementInDocument());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
// Backward selection.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, 10))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_input, 3))
.Build();
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(3u, ToTextControl(*input).selectionStart());
EXPECT_EQ(10u, ToTextControl(*input).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*input).selectionDirection());
// Ensure that the selection that was just set could be successfully
// retrieved.
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_current_selection =
AXSelection::FromCurrentSelection(ToTextControl(*input));
EXPECT_EQ(ax_selection, ax_current_selection);
}
TEST_F(AccessibilitySelectionTest, SelectingTheWholeOfTheTextField) {
SetBodyInnerHTML(R"HTML(
<p id="before">Before text field.</p>
<input id="input" value="Inside text field.">
<p id="after">After text field.</p>
)HTML");
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
ASSERT_TRUE(ToTextControl(*input).SetSelectionRange(
3u, 10u, kSelectionHasBackwardDirection));
const AXObject* ax_before = GetAXObjectByElementId("before");
ASSERT_NE(nullptr, ax_before);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
// Light tree only selection. Selects the whole of the text field.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetFocus(AXPosition::CreatePositionAfterObject(*ax_input))
.Build();
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Anchor().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Focus().AnchorNode());
EXPECT_EQ(5, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Focus().ComputeNodeAfterPosition());
// The selection in the text field should remain unchanged because the field
// is not focused.
EXPECT_EQ(3u, ToTextControl(*input).selectionStart());
EXPECT_EQ(10u, ToTextControl(*input).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*input).selectionDirection());
}
TEST_F(AccessibilitySelectionTest, SelectEachConsecutiveCharacterInTextField) {
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
TextControlElement& text_control = ToTextControl(*input);
ASSERT_LE(1u, text_control.InnerEditorValue().length());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
for (unsigned int i = 0; i < text_control.InnerEditorValue().length() - 1;
++i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, i))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_input, i + 1))
.Build();
testing::Message message;
message << "While selecting forward character "
<< static_cast<char>(text_control.InnerEditorValue()[i])
<< " at position " << i << " in text field.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i, text_control.selectionStart());
EXPECT_EQ(i + 1, text_control.selectionEnd());
EXPECT_EQ("forward", text_control.selectionDirection());
}
for (unsigned int i = text_control.InnerEditorValue().length(); i > 0; --i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, i))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_input, i - 1))
.Build();
testing::Message message;
message << "While selecting backward character "
<< static_cast<char>(text_control.InnerEditorValue()[i])
<< " at position " << i << " in text field.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i - 1, text_control.selectionStart());
EXPECT_EQ(i, text_control.selectionEnd());
EXPECT_EQ("backward", text_control.selectionDirection());
}
}
TEST_F(AccessibilitySelectionTest,
SelectEachConsecutiveCharacterInEmailFieldWithInvalidAddress) {
GetPage().GetSettings().SetScriptEnabled(true);
String valid_email = "valid@example.com";
SetBodyInnerHTML(R"HTML(
<input id="input" type="email" value=)HTML" +
valid_email + R"HTML(>
)HTML");
// Add three spaces to the start of the address to make it invalid.
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
ASSERT_NE(nullptr, script_element);
script_element->setTextContent(R"SCRIPT(
let input = document.querySelector('input');
input.focus();
input.value = input.value.padStart(3, ' ');
input.selectionStart = 0;
input.selectionEnd = input.value.length;
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
TextControlElement& text_control = ToTextControl(*input);
// The "value" attribute should not contain the extra spaces.
ASSERT_EQ(valid_email.length(), text_control.Value().length());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
// The address can still be navigated using cursor left / right, even though
// it's invalid.
for (unsigned int i = 0; i < text_control.InnerEditorValue().length() - 1;
++i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, i))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_input, i + 1))
.Build();
testing::Message message;
message << "While selecting forward character "
<< static_cast<char>(text_control.InnerEditorValue()[i])
<< " at position " << i << " in text field.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i, text_control.selectionStart());
EXPECT_EQ(i + 1, text_control.selectionEnd());
EXPECT_EQ("forward", text_control.selectionDirection());
}
for (unsigned int i = text_control.InnerEditorValue().length(); i > 0; --i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, i))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_input, i - 1))
.Build();
testing::Message message;
message << "While selecting backward character "
<< static_cast<char>(text_control.InnerEditorValue()[i])
<< " at position " << i << " in text field.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i - 1, text_control.selectionStart());
EXPECT_EQ(i, text_control.selectionEnd());
EXPECT_EQ("backward", text_control.selectionDirection());
}
}
TEST_F(AccessibilitySelectionTest, InvalidSelectionInTextField) {
SetBodyInnerHTML(R"HTML(
<p id="before">Before text field.</p>
<input id="input" value="Inside text field.">
<p id="after">After text field.</p>
)HTML");
Element* const input = GetDocument().QuerySelector(AtomicString("input"));
ASSERT_NE(nullptr, input);
ASSERT_TRUE(IsTextControl(input));
ASSERT_TRUE(ToTextControl(*input).SetSelectionRange(
3u, 10u, kSelectionHasBackwardDirection));
const AXObject* ax_before = GetAXObjectByElementId("before");
ASSERT_NE(nullptr, ax_before);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
const AXObject* ax_input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, ax_input);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_input->RoleValue());
const AXObject* ax_after = GetAXObjectByElementId("after");
ASSERT_NE(nullptr, ax_after);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
{
// Light tree only selection. Selects the whole of the text field.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetFocus(AXPosition::CreatePositionAfterObject(*ax_input))
.Build();
ax_selection.Select();
}
// Invalid selection because it crosses a user agent shadow tree boundary.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_input, 0))
.SetFocus(AXPosition::CreatePositionBeforeObject(*ax_after))
.Build();
EXPECT_FALSE(ax_selection.IsValid());
// The selection in the light DOM should remain unchanged.
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Anchor().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Focus().AnchorNode());
EXPECT_EQ(5, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Focus().ComputeNodeAfterPosition());
// The selection in the text field should remain unchanged because the field
// is not focused.
EXPECT_EQ(3u, ToTextControl(*input).selectionStart());
EXPECT_EQ(10u, ToTextControl(*input).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*input).selectionDirection());
}
TEST_F(AccessibilitySelectionTest, ForwardSelectionInTextarea) {
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
textarea->Focus(FocusOptions::Create());
ASSERT_TRUE(textarea->IsFocusedElementInDocument());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
// Forward selection.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreateFirstPositionInObject(*ax_textarea))
.SetFocus(AXPosition::CreateLastPositionInObject(*ax_textarea))
.Build();
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(0u, ToTextControl(*textarea).selectionStart());
EXPECT_EQ(53u, ToTextControl(*textarea).selectionEnd());
EXPECT_EQ("forward", ToTextControl(*textarea).selectionDirection());
// Ensure that the selection that was just set could be successfully
// retrieved.
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_current_selection =
AXSelection::FromCurrentSelection(ToTextControl(*textarea));
EXPECT_EQ(ax_selection, ax_current_selection);
}
TEST_F(AccessibilitySelectionTest, BackwardSelectionInTextarea) {
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
textarea->Focus(FocusOptions::Create());
ASSERT_TRUE(textarea->IsFocusedElementInDocument());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
// Backward selection.
AXSelection::Builder builder;
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection ax_selection =
builder
.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_textarea, 10))
.SetFocus(AXPosition::CreatePositionInTextObject(*ax_textarea, 3))
.Build();
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(3u, ToTextControl(*textarea).selectionStart());
EXPECT_EQ(10u, ToTextControl(*textarea).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*textarea).selectionDirection());
// Ensure that the selection that was just set could be successfully
// retrieved.
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_current_selection =
AXSelection::FromCurrentSelection(ToTextControl(*textarea));
EXPECT_EQ(ax_selection, ax_current_selection);
}
TEST_F(AccessibilitySelectionTest, SelectTheWholeOfTheTextarea) {
SetBodyInnerHTML(R"HTML(
<p id="before">Before textarea field.</p>
<textarea id="textarea">
Inside
textarea
field.
</textarea>
<p id="after">After textarea field.</p>
)HTML");
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
ASSERT_TRUE(ToTextControl(*textarea).SetSelectionRange(
3u, 10u, kSelectionHasBackwardDirection));
const AXObject* ax_before = GetAXObjectByElementId("before");
ASSERT_NE(nullptr, ax_before);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
// Light tree only selection. Selects the whole of the textarea field.
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetFocus(AXPosition::CreatePositionAfterObject(*ax_textarea))
.Build();
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Anchor().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Focus().AnchorNode());
EXPECT_EQ(5, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Focus().ComputeNodeAfterPosition());
// The selection in the textarea field should remain unchanged because the
// field is not focused.
EXPECT_EQ(3u, ToTextControl(*textarea).selectionStart());
EXPECT_EQ(10u, ToTextControl(*textarea).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*textarea).selectionDirection());
}
TEST_F(AccessibilitySelectionTest, SelectEachConsecutiveCharacterInTextarea) {
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
TextControlElement& text_control = ToTextControl(*textarea);
ASSERT_LE(1u, text_control.Value().length());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
for (unsigned int i = 0; i < text_control.Value().length() - 1; ++i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_textarea, i))
.SetFocus(
AXPosition::CreatePositionInTextObject(*ax_textarea, i + 1))
.Build();
testing::Message message;
message << "While selecting forward character "
<< static_cast<char>(text_control.Value()[i]) << " at position "
<< i << " in textarea.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i, text_control.selectionStart());
EXPECT_EQ(i + 1, text_control.selectionEnd());
EXPECT_EQ("forward", text_control.selectionDirection());
}
for (unsigned int i = text_control.Value().length(); i > 0; --i) {
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_textarea, i))
.SetFocus(
AXPosition::CreatePositionInTextObject(*ax_textarea, i - 1))
.Build();
testing::Message message;
message << "While selecting backward character "
<< static_cast<char>(text_control.Value()[i]) << " at position "
<< i << " in textarea.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
EXPECT_EQ(i - 1, text_control.selectionStart());
EXPECT_EQ(i, text_control.selectionEnd());
EXPECT_EQ("backward", text_control.selectionDirection());
}
}
TEST_F(AccessibilitySelectionTest, InvalidSelectionInTextarea) {
SetBodyInnerHTML(R"HTML(
<p id="before">Before textarea field.</p>
<textarea id="textarea">
Inside
textarea
field.
</textarea>
<p id="after">After textarea field.</p>
)HTML");
Element* const textarea =
GetDocument().QuerySelector(AtomicString("textarea"));
ASSERT_NE(nullptr, textarea);
ASSERT_TRUE(IsTextControl(textarea));
ASSERT_TRUE(ToTextControl(*textarea).SetSelectionRange(
3u, 10u, kSelectionHasBackwardDirection));
const AXObject* ax_before = GetAXObjectByElementId("before");
ASSERT_NE(nullptr, ax_before);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_before->RoleValue());
const AXObject* ax_textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, ax_textarea);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_textarea->RoleValue());
const AXObject* ax_after = GetAXObjectByElementId("after");
ASSERT_NE(nullptr, ax_after);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
{
// Light tree only selection. Selects the whole of the textarea field.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetFocus(AXPosition::CreatePositionAfterObject(*ax_textarea))
.Build();
ax_selection.Select();
}
// Invalid selection because it crosses a user agent shadow tree boundary.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(AXPosition::CreatePositionInTextObject(*ax_textarea, 0))
.SetFocus(AXPosition::CreatePositionBeforeObject(*ax_after))
.Build();
EXPECT_FALSE(ax_selection.IsValid());
// The selection in the light DOM should remain unchanged.
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Anchor().AnchorNode());
EXPECT_EQ(1, dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Anchor().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Focus().AnchorNode());
EXPECT_EQ(5, dom_selection.Focus().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Focus().ComputeNodeAfterPosition());
// The selection in the textarea field should remain unchanged because the
// field is not focused.
EXPECT_EQ(3u, ToTextControl(*textarea).selectionStart());
EXPECT_EQ(10u, ToTextControl(*textarea).selectionEnd());
EXPECT_EQ("backward", ToTextControl(*textarea).selectionDirection());
}
TEST_F(AccessibilitySelectionTest,
FromCurrentSelectionInContenteditableWithAffinity) {
SetBodyInnerHTML(R"HTML(
<div role="textbox" contenteditable id="contenteditable"
style="font-family: monospace; width: 15ch;">
InsideContenteditableTextboxField.
</div>
)HTML");
ASSERT_FALSE(AXSelection::FromCurrentSelection(GetDocument()).IsValid());
Element* const contenteditable =
GetDocument().QuerySelector(AtomicString("div[role=textbox]"));
ASSERT_NE(nullptr, contenteditable);
// This test should only be testing accessibility code. Ordinarily we should
// be setting up the test using Javascript in order to avoid depending on the
// internal implementation of DOM selection. However, the only way I found to
// get an upstream affinity is to send the "end" key which might be unreliable
// on certain platforms, so we modify the selection using Blink internal
// functions instead.
contenteditable->Focus();
Selection().Modify(SelectionModifyAlteration::kMove,
SelectionModifyDirection::kBackward,
TextGranularity::kDocumentBoundary, SetSelectionBy::kUser);
Selection().Modify(SelectionModifyAlteration::kMove,
SelectionModifyDirection::kForward,
TextGranularity::kLineBoundary, SetSelectionBy::kUser);
UpdateAllLifecyclePhasesForTest();
ASSERT_EQ(TextAffinity::kUpstream,
Selection().GetSelectionInDOMTree().Affinity());
const AXObject* ax_contenteditable =
GetAXObjectByElementId("contenteditable");
ASSERT_NE(nullptr, ax_contenteditable);
ASSERT_EQ(ax::mojom::Role::kTextField, ax_contenteditable->RoleValue());
const AXObject* ax_text = ax_contenteditable->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text->RoleValue());
GetDocument().ExistingAXObjectCache()->UpdateAXForAllDocuments();
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
EXPECT_TRUE(ax_selection.Anchor().IsTextPosition());
EXPECT_EQ(ax_text, ax_selection.Anchor().ContainerObject());
EXPECT_LE(15, ax_selection.Anchor().TextOffset());
EXPECT_GT(static_cast<int>(ax_text->ComputedName().length()),
ax_selection.Anchor().TextOffset());
EXPECT_EQ(TextAffinity::kUpstream, ax_selection.Anchor().Affinity());
EXPECT_TRUE(ax_selection.Focus().IsTextPosition());
EXPECT_EQ(ax_text, ax_selection.Focus().ContainerObject());
EXPECT_LE(15, ax_selection.Focus().TextOffset());
EXPECT_GT(static_cast<int>(ax_text->ComputedName().length()),
ax_selection.Focus().TextOffset());
EXPECT_EQ(TextAffinity::kUpstream, ax_selection.Focus().Affinity());
}
TEST_F(AccessibilitySelectionTest,
SelectEachConsecutiveCharacterInContenteditable) {
// The text should wrap after each word.
SetBodyInnerHTML(R"HTML(
<div id="contenteditable" contenteditable role="textbox"
style="max-width: 5px; overflow-wrap: normal;">
This is a test.
</div>
)HTML");
const Element* contenteditable =
GetDocument().QuerySelector(AtomicString("div[contenteditable]"));
ASSERT_NE(nullptr, contenteditable);
const Node* text = contenteditable->firstChild();
ASSERT_NE(nullptr, text);
ASSERT_TRUE(text->IsTextNode());
const AXObject* ax_contenteditable =
GetAXObjectByElementId("contenteditable");
ASSERT_NE(nullptr, ax_contenteditable);
ASSERT_EQ(1, ax_contenteditable->ChildCountIncludingIgnored());
const AXObject* ax_static_text =
ax_contenteditable->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_static_text->RoleValue());
String computed_name = ax_static_text->ComputedName();
ASSERT_LE(1u, computed_name.length());
for (unsigned int i = 0; i < computed_name.length() - 1; ++i) {
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(
AXPosition::CreatePositionInTextObject(*ax_static_text, i))
.SetFocus(
AXPosition::CreatePositionInTextObject(*ax_static_text, i + 1))
.Build();
testing::Message message;
message << "While selecting forward character "
<< std::u16string(1, computed_name[i]) << " at position " << i
<< " in contenteditable.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection =
Selection().GetSelectionInDOMTree();
EXPECT_EQ(text, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(text, dom_selection.Focus().AnchorNode());
// The discrepancy between DOM and AX text offsets is due to the fact that
// there is some white space in the DOM that is compressed in the
// accessibility tree.
EXPECT_EQ(static_cast<int>(i + 9),
dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(static_cast<int>(i + 10),
dom_selection.Focus().OffsetInContainerNode());
}
for (unsigned int i = computed_name.length(); i > 0; --i) {
AXSelection::Builder builder;
AXSelection ax_selection =
builder
.SetAnchor(
AXPosition::CreatePositionInTextObject(*ax_static_text, i))
.SetFocus(
AXPosition::CreatePositionInTextObject(*ax_static_text, i - 1))
.Build();
testing::Message message;
message << "While selecting backward character "
<< std::u16string(1, computed_name[i]) << " at position " << i
<< " in contenteditable.";
SCOPED_TRACE(message);
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection =
Selection().GetSelectionInDOMTree();
EXPECT_EQ(text, dom_selection.Anchor().AnchorNode());
EXPECT_EQ(text, dom_selection.Focus().AnchorNode());
// The discrepancy between DOM and AX text offsets is due to the fact that
// there is some white space in the DOM that is compressed in the
// accessibility tree.
EXPECT_EQ(static_cast<int>(i + 9),
dom_selection.Anchor().OffsetInContainerNode());
EXPECT_EQ(static_cast<int>(i + 8),
dom_selection.Focus().OffsetInContainerNode());
}
}
TEST_F(AccessibilitySelectionTest, SelectionWithEqualBaseAndExtent) {
SetBodyInnerHTML(R"HTML(
<select id="sel"><option>1</option></select>
)HTML");
AXObject* ax_sel =
GetAXObjectByElementId("sel")->FirstChildIncludingIgnored();
AXPosition ax_position = AXPosition::CreatePositionBeforeObject(*ax_sel);
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetAnchor(ax_position).SetFocus(ax_position).Build();
}
TEST_F(AccessibilitySelectionTest, InvalidSelectionOnAShadowRoot) {
GetPage().GetSettings().SetScriptEnabled(true);
SetBodyInnerHTML(R"HTML(
<div id="container">
</div>
)HTML");
Element* const script_element =
GetDocument().CreateRawElement(html_names::kScriptTag);
script_element->setTextContent(R"SCRIPT(
var container = document.getElementById("container");
var shadow = container.attachShadow({mode: 'open'});
var button = document.createElement("button");
button.id = "button";
shadow.appendChild(button);
)SCRIPT");
GetDocument().body()->AppendChild(script_element);
UpdateAllLifecyclePhasesForTest();
Node* shadow_root = GetElementById("container")->GetShadowRoot();
const Position base = Position::EditingPositionOf(shadow_root, 0);
const Position extent = Position::EditingPositionOf(shadow_root, 1);
const auto selection =
SelectionInDOMTree::Builder().SetBaseAndExtent(base, extent).Build();
EXPECT_FALSE(AXSelection::FromSelection(selection).IsValid());
}
//
// Declarative tests.
//
TEST_F(AccessibilitySelectionTest, ARIAHidden) {
RunSelectionTest("aria-hidden");
}
TEST_F(AccessibilitySelectionTest, List) {
RunSelectionTest("list");
}
TEST_F(AccessibilitySelectionTest, ParagraphPresentational) {
// The focus of the selection is an "after children" position on a paragraph
// with role="presentation" and in which the last child is an empty div. In
// other words, both the paragraph and its last child are ignored in the
// accessibility tree. In order to become valid, the focus should move to
// before the next unignored child of the presentational paragraph's unignored
// parent, which in this case is another paragraph that comes after the
// presentational one.
RunSelectionTest("paragraph-presentational");
}
TEST_F(AccessibilitySelectionTest, SVG) {
RunSelectionTest("svg");
}
TEST_F(AccessibilitySelectionTest, Table) {
RunSelectionTest("table");
}
} // namespace test
} // namespace blink