blob: c3ad8c92ad59c34536769c5c685322e4d00ad765 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// 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 "testing/gtest/include/gtest/gtest.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_template.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_div_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_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");
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);
UpdateAllLifecyclePhasesForTest();
const AXObject* ax_static_text_1 =
GetAXObjectByElementId("paragraph1")->FirstChild();
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());
const auto ax_selection = AXSelection::FromCurrentSelection(GetDocument());
ASSERT_TRUE(ax_selection.IsValid());
EXPECT_TRUE(ax_selection.Base().IsTextPosition());
EXPECT_EQ(ax_static_text_1, ax_selection.Base().ContainerObject());
EXPECT_EQ(3, ax_selection.Base().TextOffset());
EXPECT_FALSE(ax_selection.Extent().IsTextPosition());
EXPECT_EQ(ax_paragraph_2, ax_selection.Extent().ContainerObject());
EXPECT_EQ(1, ax_selection.Extent().ChildIndex());
EXPECT_EQ(
"++<Paragraph>\n"
"++++<StaticText: Hel^lo.>\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")->FirstChild();
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
.SetBase(AXPosition::CreatePositionInTextObject(*ax_static_text_1, 3))
.SetExtent(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(
"++<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")->FirstChild();
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
.SetBase(AXPosition::CreatePositionInTextObject(*ax_static_text_1, 3))
.SetExtent(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("p")->firstChild();
ASSERT_NE(nullptr, text);
ASSERT_TRUE(text->IsTextNode());
const AXObject* ax_static_text =
GetAXObjectByElementId("paragraph")->FirstChild();
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.SetBase(ax_base).SetExtent(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Base().AnchorNode());
EXPECT_EQ(3, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(
"++<Paragraph>\n"
"++++<StaticText: Hel^lo|>\n",
GetSelectionText(ax_selection));
}
TEST_F(AccessibilitySelectionTest, SetSelectionInTextWithWhiteSpace) {
SetBodyInnerHTML(R"HTML(<p id="paragraph"> Hello</p>)HTML");
const Node* text = GetDocument().QuerySelector("p")->firstChild();
ASSERT_NE(nullptr, text);
ASSERT_TRUE(text->IsTextNode());
const AXObject* ax_static_text =
GetAXObjectByElementId("paragraph")->FirstChild();
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.SetBase(ax_base).SetExtent(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Base().AnchorNode());
EXPECT_EQ(8, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Extent().AnchorNode());
EXPECT_EQ(10, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(
"++<Paragraph>\n"
"++++<StaticText: Hel^lo|>\n",
GetSelectionText(ax_selection));
}
//
// Get selection tests.
// Retrieving a selection with endpoints which have no corresponding objects in
// the accessibility tree, e.g. which are aria-hidden, should shrink or extend
// the |AXSelection| to valid endpoints.
//
TEST_F(AccessibilitySelectionTest, SetSelectionInARIAHidden) {
SetBodyInnerHTML(R"HTML(
<div id="main" role="main">
<p id="beforeHidden">Before aria-hidden.</p>
<p id="hidden1" aria-hidden="true">Aria-hidden 1.</p>
<p id="betweenHidden">In between two aria-hidden elements.</p>
<p id="hidden2" aria-hidden="true">Aria-hidden 2.</p>
<p id="afterHidden">After aria-hidden.</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_between = GetAXObjectByElementId("betweenHidden");
ASSERT_NE(nullptr, ax_between);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_between->RoleValue());
const AXObject* ax_after = GetAXObjectByElementId("afterHidden");
ASSERT_NE(nullptr, ax_after);
ASSERT_EQ(ax::mojom::Role::kParagraph, ax_after->RoleValue());
ASSERT_NE(nullptr, GetAXObjectByElementId("hidden1"));
ASSERT_TRUE(GetAXObjectByElementId("hidden1")->AccessibilityIsIgnored());
ASSERT_NE(nullptr, GetAXObjectByElementId("hidden2"));
ASSERT_TRUE(GetAXObjectByElementId("hidden2")->AccessibilityIsIgnored());
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::kShrinkToValidDOMRange);
const auto ax_selection_extend = AXSelection::FromSelection(
selection, AXSelectionBehavior::kExtendToValidDOMRange);
// The shrunk selection should encompass only the |AXObject| between the two
// aria-hidden elements and nothing else. This means that its anchor should be
// before and its focus after the |AXObject| in question.
ASSERT_FALSE(ax_selection_shrink.Base().IsTextPosition());
EXPECT_EQ(ax_main, ax_selection_shrink.Base().ContainerObject());
EXPECT_EQ(ax_between->IndexInParent(),
ax_selection_shrink.Base().ChildIndex());
ASSERT_FALSE(ax_selection_shrink.Extent().IsTextPosition());
EXPECT_EQ(ax_between, ax_selection_shrink.Extent().ContainerObject());
EXPECT_EQ(1, ax_selection_shrink.Extent().ChildIndex());
// The extended selection should start after the children of the paragraph
// before the first aria-hidden element and end right before the paragraph
// after the last aria-hidden element.
EXPECT_FALSE(ax_selection_extend.Base().IsTextPosition());
EXPECT_EQ(ax_before, ax_selection_extend.Base().ContainerObject());
EXPECT_EQ(1, ax_selection_extend.Base().ChildIndex());
EXPECT_FALSE(ax_selection_extend.Extent().IsTextPosition());
EXPECT_EQ(ax_main, ax_selection_extend.Extent().ContainerObject());
EXPECT_EQ(ax_after->IndexInParent(),
ax_selection_extend.Extent().ChildIndex());
// 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
// aria-hidden. However, the AX selections should still differ if converted to
// DOM selections.
const std::string selection_text(
"++<Main>\n"
"++++<Paragraph>\n"
"++++++<StaticText: Before aria-hidden.>\n"
"^++++<Paragraph>\n"
"++++++<StaticText: In between two aria-hidden elements.>\n"
"|++++<Paragraph>\n"
"++++++<StaticText: After aria-hidden.>\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* 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->FirstChild();
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->LastChild();
ASSERT_NE(nullptr, ax_text_2);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_2->RoleValue());
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetBase(AXPosition::CreateFirstPositionInObject(*ax_bullet_1))
.SetExtent(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 "before children" position at the
// first <li>.
ax_selection.Select(AXSelectionBehavior::kShrinkToValidDOMRange);
const SelectionInDOMTree shrunk_selection =
Selection().GetSelectionInDOMTree();
EXPECT_EQ(item_1, shrunk_selection.Base().AnchorNode());
EXPECT_TRUE(shrunk_selection.Base().IsBeforeChildren());
ASSERT_TRUE(shrunk_selection.Extent().IsOffsetInAnchor());
EXPECT_EQ(text_2, shrunk_selection.Extent().AnchorNode());
EXPECT_EQ(7, shrunk_selection.Extent().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::kExtendToValidDOMRange);
const SelectionInDOMTree extended_selection =
Selection().GetSelectionInDOMTree();
ASSERT_TRUE(extended_selection.Base().IsOffsetInAnchor());
EXPECT_EQ(item_1->parentNode(), extended_selection.Base().AnchorNode());
EXPECT_EQ(static_cast<int>(item_1->NodeIndex()),
extended_selection.Base().OffsetInContainerNode());
ASSERT_TRUE(extended_selection.Extent().IsOffsetInAnchor());
EXPECT_EQ(text_2, extended_selection.Extent().AnchorNode());
EXPECT_EQ(7, extended_selection.Extent().OffsetInContainerNode());
// The |AXSelection| should remain unaffected by any shrinking and should
// include both list bullets.
EXPECT_EQ(
"++<Main>\n"
"++++<List>\n"
"++++++<ListItem>\n"
"++++++++<ListMarker: \xE2\x80\xA2 >\n"
"++++++++<StaticText: Item 1.>\n"
"++++++<ListItem>\n"
"++++++++<ListMarker: \xE2\x80\xA2 >\n"
"++++++++<StaticText: Item 2.|>\n",
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("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());
EXPECT_TRUE(ax_selection.Base().IsTextPosition());
EXPECT_EQ(ax_input, ax_selection.Base().ContainerObject());
EXPECT_EQ(0, ax_selection.Base().TextOffset());
EXPECT_TRUE(ax_selection.Extent().IsTextPosition());
EXPECT_EQ(ax_input, ax_selection.Extent().ContainerObject());
EXPECT_EQ(18, ax_selection.Extent().TextOffset());
}
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("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());
EXPECT_TRUE(ax_selection.Base().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Base().ContainerObject());
EXPECT_EQ(0, ax_selection.Base().TextOffset());
EXPECT_TRUE(ax_selection.Extent().IsTextPosition());
EXPECT_EQ(ax_textarea, ax_selection.Extent().ContainerObject());
EXPECT_EQ(53, ax_selection.Extent().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("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.SetBase(AXPosition::CreateFirstPositionInObject(*ax_input))
.SetExtent(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());
}
TEST_F(AccessibilitySelectionTest, BackwardSelectionInTextField) {
SetBodyInnerHTML(R"HTML(
<input id="input" value="Inside text field.">
)HTML");
Element* const input = GetDocument().QuerySelector("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.SetBase(AXPosition::CreatePositionInTextObject(*ax_input, 10))
.SetExtent(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());
}
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("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.SetBase(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetExtent(AXPosition::CreatePositionAfterObject(*ax_input))
.Build();
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Base().AnchorNode());
EXPECT_EQ(1, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Base().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Extent().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, 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("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());
{
// Light tree only selection. Selects the whole of the text field.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetBase(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetExtent(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.SetBase(AXPosition::CreatePositionInTextObject(*ax_input, 0))
.SetExtent(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.Base().AnchorNode());
EXPECT_EQ(1, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Base().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Extent().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("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.SetBase(AXPosition::CreateFirstPositionInObject(*ax_textarea))
.SetExtent(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());
}
TEST_F(AccessibilitySelectionTest, BackwardSelectionInTextarea) {
SetBodyInnerHTML(R"HTML(
<textarea id="textarea">
Inside
textarea
field.
</textarea>
)HTML");
Element* const textarea = GetDocument().QuerySelector("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;
AXSelection ax_selection =
builder.SetBase(AXPosition::CreatePositionInTextObject(*ax_textarea, 10))
.SetExtent(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());
}
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("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.
AXSelection::Builder builder;
AXSelection ax_selection =
builder.SetBase(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetExtent(AXPosition::CreatePositionAfterObject(*ax_textarea))
.Build();
EXPECT_TRUE(ax_selection.Select());
const SelectionInDOMTree dom_selection = Selection().GetSelectionInDOMTree();
EXPECT_EQ(GetDocument().body(), dom_selection.Base().AnchorNode());
EXPECT_EQ(1, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Base().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Extent().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, 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("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.SetBase(AXPosition::CreatePositionBeforeObject(*ax_before))
.SetExtent(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.SetBase(AXPosition::CreatePositionInTextObject(*ax_textarea, 0))
.SetExtent(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.Base().AnchorNode());
EXPECT_EQ(1, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(GetElementById("before"),
dom_selection.Base().ComputeNodeAfterPosition());
EXPECT_EQ(GetDocument().body(), dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
EXPECT_EQ(GetElementById("after"),
dom_selection.Extent().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());
}
//
// Declarative tests.
//
TEST_F(AccessibilitySelectionTest, ARIAHidden) {
RunSelectionTest("aria-hidden");
}
TEST_F(AccessibilitySelectionTest, List) {
RunSelectionTest("list");
}
TEST_F(AccessibilitySelectionTest, table) {
RunSelectionTest("table");
}
} // namespace test
} // namespace blink