blob: ad1f51af1488800169a6515fb1ca0a651c402ab6 [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_object.h"
#include <memory>
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/testing/accessibility_test.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_mode.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_tree_id.h"
namespace blink {
namespace test {
using testing::Each;
using testing::Property;
using testing::SafeMatcherCast;
TEST_F(AccessibilityTest, GetClosestElementChecksStartingNode) {
SetBodyInnerHTML(R"HTML(<button id="button">button</button>)HTML");
const AXObject* button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
const Element* closestElement = button->GetClosestElement();
ASSERT_NE(nullptr, closestElement);
EXPECT_TRUE(closestElement == button->GetElement());
}
TEST_F(AccessibilityTest, GetClosestElementSearchesAmongAncestors) {
SetBodyInnerHTML(R"HTML(
<style>
button::before{
content: "Content";
}
</style>
<button id="button">button</button>
)HTML");
AXObject* button = GetAXObjectByElementId("button");
button->LoadInlineTextBoxes();
// Guaranteed to have no element since this should be the AX node created from
// pseudo element content
const AXObject* nodeWithNoElement =
button->DeepestFirstChildIncludingIgnored()->ParentObject();
ASSERT_EQ(nullptr, nodeWithNoElement->GetElement());
EXPECT_EQ(nodeWithNoElement->GetClosestElement(),
button->GetElement()->GetPseudoElement(kPseudoIdBefore));
}
TEST_F(AccessibilityTest, IsEditableInTextField) {
SetBodyInnerHTML(R"HTML(
<input type="text" id="input" value="Test">
<textarea id="textarea">
Test
</textarea>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, input);
const AXObject* input_text =
input->FirstChildIncludingIgnored()->UnignoredChildAt(0);
ASSERT_NE(nullptr, input_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, input_text->RoleValue());
const AXObject* textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, textarea);
const AXObject* textarea_text =
textarea->FirstChildIncludingIgnored()->UnignoredChildAt(0);
ASSERT_NE(nullptr, textarea_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, textarea_text->RoleValue());
EXPECT_FALSE(root->IsEditable());
EXPECT_TRUE(input->IsEditable());
EXPECT_TRUE(input_text->IsEditable());
EXPECT_TRUE(textarea->IsEditable());
EXPECT_TRUE(textarea_text->IsEditable());
EXPECT_FALSE(root->IsEditableRoot());
EXPECT_FALSE(input->IsEditableRoot());
EXPECT_FALSE(input_text->IsEditableRoot());
EXPECT_FALSE(textarea->IsEditableRoot());
EXPECT_FALSE(textarea_text->IsEditableRoot());
EXPECT_FALSE(root->HasContentEditableAttributeSet());
EXPECT_FALSE(input->HasContentEditableAttributeSet());
EXPECT_FALSE(input_text->HasContentEditableAttributeSet());
EXPECT_FALSE(textarea->HasContentEditableAttributeSet());
EXPECT_FALSE(textarea_text->HasContentEditableAttributeSet());
EXPECT_FALSE(root->IsMultiline());
EXPECT_FALSE(input->IsMultiline());
EXPECT_FALSE(input_text->IsMultiline());
EXPECT_TRUE(textarea->IsMultiline());
EXPECT_FALSE(textarea_text->IsMultiline());
EXPECT_FALSE(root->IsRichlyEditable());
EXPECT_FALSE(input->IsRichlyEditable());
EXPECT_FALSE(input_text->IsRichlyEditable());
EXPECT_FALSE(textarea->IsRichlyEditable());
EXPECT_FALSE(textarea_text->IsRichlyEditable());
}
TEST_F(AccessibilityTest, IsEditableInTextFieldWithContentEditableTrue) {
SetBodyInnerHTML(R"HTML(
<!-- This is technically an authoring error, but we should still handle
it correctly. -->
<input type="text" id="input" value="Test" contenteditable="true">
<textarea id="textarea" contenteditable="true">
Test
</textarea>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, input);
const AXObject* input_text =
input->FirstChildIncludingIgnored()->UnignoredChildAt(0);
ASSERT_NE(nullptr, input_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, input_text->RoleValue());
const AXObject* textarea = GetAXObjectByElementId("textarea");
ASSERT_NE(nullptr, textarea);
const AXObject* textarea_text =
textarea->FirstChildIncludingIgnored()->UnignoredChildAt(0);
ASSERT_NE(nullptr, textarea_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, textarea_text->RoleValue());
EXPECT_FALSE(root->IsEditable());
EXPECT_TRUE(input->IsEditable());
EXPECT_TRUE(input_text->IsEditable());
EXPECT_TRUE(textarea->IsEditable());
EXPECT_TRUE(textarea_text->IsEditable());
EXPECT_FALSE(root->IsEditableRoot());
EXPECT_FALSE(input->IsEditableRoot());
EXPECT_FALSE(input_text->IsEditableRoot());
EXPECT_FALSE(textarea->IsEditableRoot());
EXPECT_FALSE(textarea_text->IsEditableRoot());
EXPECT_FALSE(root->HasContentEditableAttributeSet());
EXPECT_TRUE(input->HasContentEditableAttributeSet());
EXPECT_FALSE(input_text->HasContentEditableAttributeSet());
EXPECT_TRUE(textarea->HasContentEditableAttributeSet());
EXPECT_FALSE(textarea_text->HasContentEditableAttributeSet());
EXPECT_FALSE(root->IsMultiline());
EXPECT_FALSE(input->IsMultiline());
EXPECT_FALSE(input_text->IsMultiline());
EXPECT_TRUE(textarea->IsMultiline());
EXPECT_FALSE(textarea_text->IsMultiline());
EXPECT_FALSE(root->IsRichlyEditable());
EXPECT_FALSE(input->IsRichlyEditable());
EXPECT_FALSE(input_text->IsRichlyEditable());
EXPECT_FALSE(textarea->IsRichlyEditable());
EXPECT_FALSE(textarea_text->IsRichlyEditable());
}
TEST_F(AccessibilityTest, IsEditableInContentEditable) {
// On purpose, also add the textbox role to ensure that it won't affect the
// contenteditable state.
SetBodyInnerHTML(R"HTML(
<div role="textbox" contenteditable="true" id="outerContenteditable">
Test
<div contenteditable="plaintext-only" id="innerContenteditable">
Test
</div>
</div>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* outer_contenteditable =
GetAXObjectByElementId("outerContenteditable");
ASSERT_NE(nullptr, outer_contenteditable);
const AXObject* outer_contenteditable_text =
outer_contenteditable->UnignoredChildAt(0);
ASSERT_NE(nullptr, outer_contenteditable_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
outer_contenteditable_text->RoleValue());
const AXObject* inner_contenteditable =
GetAXObjectByElementId("innerContenteditable");
ASSERT_NE(nullptr, inner_contenteditable);
const AXObject* inner_contenteditable_text =
inner_contenteditable->UnignoredChildAt(0);
ASSERT_NE(nullptr, inner_contenteditable_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
inner_contenteditable_text->RoleValue());
EXPECT_FALSE(root->IsEditable());
EXPECT_TRUE(outer_contenteditable->IsEditable());
EXPECT_TRUE(outer_contenteditable_text->IsEditable());
EXPECT_TRUE(inner_contenteditable->IsEditable());
EXPECT_TRUE(inner_contenteditable_text->IsEditable());
EXPECT_FALSE(root->IsEditableRoot());
EXPECT_TRUE(outer_contenteditable->IsEditableRoot());
EXPECT_FALSE(outer_contenteditable_text->IsEditableRoot());
EXPECT_TRUE(inner_contenteditable->IsEditableRoot());
EXPECT_FALSE(inner_contenteditable_text->IsEditableRoot());
EXPECT_FALSE(root->HasContentEditableAttributeSet());
EXPECT_TRUE(outer_contenteditable->HasContentEditableAttributeSet());
EXPECT_FALSE(outer_contenteditable_text->HasContentEditableAttributeSet());
EXPECT_TRUE(inner_contenteditable->HasContentEditableAttributeSet());
EXPECT_FALSE(inner_contenteditable_text->HasContentEditableAttributeSet());
EXPECT_FALSE(root->IsMultiline());
EXPECT_TRUE(outer_contenteditable->IsMultiline());
EXPECT_FALSE(outer_contenteditable_text->IsMultiline());
EXPECT_TRUE(inner_contenteditable->IsMultiline());
EXPECT_FALSE(inner_contenteditable_text->IsMultiline());
EXPECT_FALSE(root->IsRichlyEditable());
EXPECT_TRUE(outer_contenteditable->IsRichlyEditable());
EXPECT_TRUE(outer_contenteditable_text->IsRichlyEditable());
// contenteditable="plaintext-only".
EXPECT_FALSE(inner_contenteditable->IsRichlyEditable());
EXPECT_FALSE(inner_contenteditable_text->IsRichlyEditable());
}
TEST_F(AccessibilityTest, IsEditableInCanvasFallback) {
SetBodyInnerHTML(R"HTML(
<canvas id="canvas" width="300" height="300">
<input id="input" value="Test">
<div contenteditable="true" id="outerContenteditable">
Test
<div contenteditable="plaintext-only" id="innerContenteditable">
Test
</div>
</div>
</canvas>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* canvas = GetAXObjectByElementId("canvas");
ASSERT_NE(nullptr, canvas);
const AXObject* input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, input);
const AXObject* input_text =
input->FirstChildIncludingIgnored()->UnignoredChildAt(0);
ASSERT_NE(nullptr, input_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, input_text->RoleValue());
const AXObject* outer_contenteditable =
GetAXObjectByElementId("outerContenteditable");
ASSERT_NE(nullptr, outer_contenteditable);
const AXObject* outer_contenteditable_text =
outer_contenteditable->UnignoredChildAt(0);
ASSERT_NE(nullptr, outer_contenteditable_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
outer_contenteditable_text->RoleValue());
const AXObject* inner_contenteditable =
GetAXObjectByElementId("innerContenteditable");
ASSERT_NE(nullptr, inner_contenteditable);
const AXObject* inner_contenteditable_text =
inner_contenteditable->UnignoredChildAt(0);
ASSERT_NE(nullptr, inner_contenteditable_text);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText,
inner_contenteditable_text->RoleValue());
EXPECT_FALSE(root->IsEditable());
EXPECT_FALSE(canvas->IsEditable());
EXPECT_TRUE(input->IsEditable());
EXPECT_TRUE(input_text->IsEditable());
EXPECT_TRUE(outer_contenteditable->IsEditable());
EXPECT_TRUE(outer_contenteditable_text->IsEditable());
EXPECT_TRUE(inner_contenteditable->IsEditable());
EXPECT_TRUE(inner_contenteditable_text->IsEditable());
EXPECT_FALSE(root->IsEditableRoot());
EXPECT_FALSE(canvas->IsEditableRoot());
EXPECT_FALSE(input->IsEditableRoot());
EXPECT_FALSE(input_text->IsEditableRoot());
EXPECT_TRUE(outer_contenteditable->IsEditableRoot());
EXPECT_FALSE(outer_contenteditable_text->IsEditableRoot());
EXPECT_TRUE(inner_contenteditable->IsEditableRoot());
EXPECT_FALSE(inner_contenteditable_text->IsEditableRoot());
EXPECT_FALSE(root->HasContentEditableAttributeSet());
EXPECT_FALSE(canvas->HasContentEditableAttributeSet());
EXPECT_FALSE(input->HasContentEditableAttributeSet());
EXPECT_FALSE(input_text->HasContentEditableAttributeSet());
EXPECT_TRUE(outer_contenteditable->HasContentEditableAttributeSet());
EXPECT_FALSE(outer_contenteditable_text->HasContentEditableAttributeSet());
EXPECT_TRUE(inner_contenteditable->HasContentEditableAttributeSet());
EXPECT_FALSE(inner_contenteditable_text->HasContentEditableAttributeSet());
EXPECT_FALSE(root->IsMultiline());
EXPECT_FALSE(canvas->IsMultiline());
EXPECT_FALSE(input->IsMultiline());
EXPECT_FALSE(input_text->IsMultiline());
EXPECT_TRUE(outer_contenteditable->IsMultiline());
EXPECT_FALSE(outer_contenteditable_text->IsMultiline());
EXPECT_TRUE(inner_contenteditable->IsMultiline());
EXPECT_FALSE(inner_contenteditable_text->IsMultiline());
EXPECT_FALSE(root->IsRichlyEditable());
EXPECT_FALSE(canvas->IsRichlyEditable());
EXPECT_FALSE(input->IsRichlyEditable());
EXPECT_FALSE(input_text->IsRichlyEditable());
EXPECT_TRUE(outer_contenteditable->IsRichlyEditable());
EXPECT_TRUE(outer_contenteditable_text->IsRichlyEditable());
EXPECT_FALSE(inner_contenteditable->IsRichlyEditable());
EXPECT_FALSE(inner_contenteditable_text->IsRichlyEditable());
}
TEST_F(AccessibilityTest, DetachedIsIgnored) {
SetBodyInnerHTML(R"HTML(<button id="button">button</button>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
AXObject* button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
EXPECT_FALSE(button->IsDetached());
EXPECT_FALSE(button->AccessibilityIsIgnored());
GetAXObjectCache().Remove(button->GetNode());
EXPECT_TRUE(button->IsDetached());
EXPECT_TRUE(button->AccessibilityIsIgnored());
EXPECT_FALSE(button->AccessibilityIsIgnoredButIncludedInTree());
}
TEST_F(AccessibilityTest, UnignoredChildren) {
SetBodyInnerHTML(R"HTML(This is a test with
<p role="presentation">
ignored objects
</p>
<p>
which are at multiple
</p>
<p role="presentation">
<p role="presentation">
depth levels
</p>
in the accessibility tree.
</p>)HTML");
const AXObject* ax_body = GetAXRootObject()->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_body);
ASSERT_EQ(5, ax_body->UnignoredChildCount());
EXPECT_EQ(ax::mojom::blink::Role::kStaticText,
ax_body->UnignoredChildAt(0)->RoleValue());
EXPECT_EQ("This is a test with",
ax_body->UnignoredChildAt(0)->ComputedName());
EXPECT_EQ(ax::mojom::blink::Role::kStaticText,
ax_body->UnignoredChildAt(1)->RoleValue());
EXPECT_EQ("ignored objects", ax_body->UnignoredChildAt(1)->ComputedName());
EXPECT_EQ(ax::mojom::blink::Role::kParagraph,
ax_body->UnignoredChildAt(2)->RoleValue());
EXPECT_EQ(ax::mojom::blink::Role::kStaticText,
ax_body->UnignoredChildAt(3)->RoleValue());
EXPECT_EQ("depth levels", ax_body->UnignoredChildAt(3)->ComputedName());
EXPECT_EQ(ax::mojom::blink::Role::kStaticText,
ax_body->UnignoredChildAt(4)->RoleValue());
EXPECT_EQ("in the accessibility tree.",
ax_body->UnignoredChildAt(4)->ComputedName());
}
TEST_F(AccessibilityTest, SimpleTreeNavigation) {
SetBodyInnerHTML(R"HTML(<input id="input" type="text" value="value">
<div id="ignored_a" aria-hidden="true" lang="en-US"></div>
<p id="paragraph">hello<br id="br">there</p>
<span id="ignored_b" aria-hidden="true" lang="fr-CA"></span>
<button id="button">button</button>)HTML");
AXObject* body = GetAXBodyObject();
ASSERT_NE(nullptr, body);
body->LoadInlineTextBoxes();
const AXObject* input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, input);
ASSERT_NE(nullptr, GetAXObjectByElementId("ignored_a"));
ASSERT_TRUE(GetAXObjectByElementId("ignored_a")->AccessibilityIsIgnored());
const AXObject* paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, paragraph);
const AXObject* br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, br);
ASSERT_NE(nullptr, GetAXObjectByElementId("ignored_b"));
ASSERT_TRUE(GetAXObjectByElementId("ignored_b")->AccessibilityIsIgnored());
const AXObject* button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
EXPECT_EQ(input, body->FirstChildIncludingIgnored());
EXPECT_EQ(button, body->LastChildIncludingIgnored());
ASSERT_NE(nullptr, paragraph->FirstChildIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
paragraph->FirstChildIncludingIgnored()->RoleValue());
ASSERT_NE(nullptr, paragraph->LastChildIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
paragraph->LastChildIncludingIgnored()->RoleValue());
ASSERT_NE(nullptr, paragraph->FirstChildIncludingIgnored()->ParentObject());
EXPECT_EQ(ax::mojom::Role::kStaticText,
paragraph->DeepestFirstChildIncludingIgnored()
->ParentObject()
->RoleValue());
ASSERT_NE(nullptr,
paragraph->DeepestLastChildIncludingIgnored()->ParentObject());
EXPECT_EQ(ax::mojom::Role::kStaticText,
paragraph->DeepestLastChildIncludingIgnored()
->ParentObject()
->RoleValue());
EXPECT_EQ(paragraph->PreviousSiblingIncludingIgnored(),
GetAXObjectByElementId("ignored_a"));
EXPECT_EQ(GetAXObjectByElementId("ignored_a"),
input->NextSiblingIncludingIgnored());
ASSERT_NE(nullptr, br->NextSiblingIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
br->NextSiblingIncludingIgnored()->RoleValue());
ASSERT_NE(nullptr, br->PreviousSiblingIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
br->PreviousSiblingIncludingIgnored()->RoleValue());
EXPECT_EQ(paragraph->UnignoredPreviousSibling(), input);
EXPECT_EQ(paragraph, input->UnignoredNextSibling());
ASSERT_NE(nullptr, br->UnignoredNextSibling());
EXPECT_EQ(ax::mojom::Role::kStaticText,
br->UnignoredNextSibling()->RoleValue());
ASSERT_NE(nullptr, br->UnignoredPreviousSibling());
EXPECT_EQ(ax::mojom::Role::kStaticText,
br->UnignoredPreviousSibling()->RoleValue());
ASSERT_NE(nullptr, button->FirstChildIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
button->FirstChildIncludingIgnored()->RoleValue());
ASSERT_NE(nullptr, button->LastChildIncludingIgnored());
EXPECT_EQ(ax::mojom::Role::kStaticText,
button->LastChildIncludingIgnored()->RoleValue());
ASSERT_NE(nullptr,
button->DeepestFirstChildIncludingIgnored()->ParentObject());
EXPECT_EQ(ax::mojom::Role::kStaticText,
paragraph->DeepestFirstChildIncludingIgnored()
->ParentObject()
->RoleValue());
}
TEST_F(AccessibilityTest, LangAttrInteresting) {
SetBodyInnerHTML(R"HTML(
<div id="A"><span>some text</span></div>
<div id="B"><span lang='en'>some text</span></div>
)HTML");
const AXObject* obj_a = GetAXObjectByElementId("A");
ASSERT_NE(nullptr, obj_a);
ASSERT_EQ(obj_a->ChildCountIncludingIgnored(), 1);
// A.span will be excluded from tree as it isn't semantically interesting.
// Instead its kStaticText child will be promoted.
const AXObject* span_1 = obj_a->ChildAtIncludingIgnored(0);
ASSERT_NE(nullptr, span_1);
EXPECT_EQ(ax::mojom::Role::kStaticText, span_1->RoleValue());
const AXObject* obj_b = GetAXObjectByElementId("B");
ASSERT_NE(nullptr, obj_b);
ASSERT_EQ(obj_b->ChildCountIncludingIgnored(), 1);
// B.span will be present as the lang attribute is semantically interesting.
const AXObject* span_2 = obj_b->ChildAtIncludingIgnored(0);
ASSERT_NE(nullptr, span_2);
EXPECT_EQ(ax::mojom::Role::kGenericContainer, span_2->RoleValue());
}
TEST_F(AccessibilityTest, LangAttrInterestingHidden) {
SetBodyInnerHTML(R"HTML(
<div id="A"><span lang='en' aria-hidden='true'>some text</span></div>
)HTML");
const AXObject* obj_a = GetAXObjectByElementId("A");
ASSERT_NE(nullptr, obj_a);
ASSERT_EQ(obj_a->ChildCountIncludingIgnored(), 1);
// A.span will be present as the lang attribute is semantically interesting.
const AXObject* span_1 = obj_a->ChildAtIncludingIgnored(0);
ASSERT_NE(nullptr, span_1);
EXPECT_EQ(ax::mojom::Role::kGenericContainer, span_1->RoleValue());
EXPECT_TRUE(span_1->AccessibilityIsIgnoredButIncludedInTree());
}
TEST_F(AccessibilityTest, TreeNavigationWithIgnoredContainer) {
// Setup the following tree :
// ++A
// ++IGNORED
// ++++B
// ++C
// So that nodes [A, B, C] are siblings
SetBodyInnerHTML(R"HTML(
<p id="A">some text</p>
<div>
<p id="B">nested text</p>
</div>
<p id="C">more text</p>
)HTML");
AXObject* root = GetAXRootObject();
root->LoadInlineTextBoxes();
const AXObject* body = GetAXBodyObject();
ASSERT_EQ(3, body->ChildCountIncludingIgnored());
ASSERT_EQ(1, body->ChildAtIncludingIgnored(1)->ChildCountIncludingIgnored());
ASSERT_FALSE(root->AccessibilityIsIgnored());
ASSERT_TRUE(body->AccessibilityIsIgnored());
const AXObject* obj_a = GetAXObjectByElementId("A");
ASSERT_NE(nullptr, obj_a);
ASSERT_FALSE(obj_a->AccessibilityIsIgnored());
const AXObject* obj_a_text = obj_a->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, obj_a_text);
EXPECT_EQ(ax::mojom::Role::kStaticText, obj_a_text->RoleValue());
const AXObject* obj_b = GetAXObjectByElementId("B");
ASSERT_NE(nullptr, obj_b);
ASSERT_FALSE(obj_b->AccessibilityIsIgnored());
const AXObject* obj_b_text = obj_b->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, obj_b_text);
EXPECT_EQ(ax::mojom::Role::kStaticText, obj_b_text->RoleValue());
const AXObject* obj_c = GetAXObjectByElementId("C");
ASSERT_NE(nullptr, obj_c);
ASSERT_FALSE(obj_c->AccessibilityIsIgnored());
const AXObject* obj_c_text = obj_c->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, obj_c_text);
EXPECT_EQ(ax::mojom::Role::kStaticText, obj_c_text->RoleValue());
const AXObject* obj_ignored = body->ChildAtIncludingIgnored(1);
ASSERT_NE(nullptr, obj_ignored);
ASSERT_TRUE(obj_ignored->AccessibilityIsIgnored());
EXPECT_EQ(root, obj_a->ParentObjectUnignored());
EXPECT_EQ(body, obj_a->ParentObjectIncludedInTree());
EXPECT_EQ(root, obj_b->ParentObjectUnignored());
EXPECT_EQ(obj_ignored, obj_b->ParentObjectIncludedInTree());
EXPECT_EQ(root, obj_c->ParentObjectUnignored());
EXPECT_EQ(body, obj_c->ParentObjectIncludedInTree());
EXPECT_EQ(obj_b, obj_ignored->FirstChildIncludingIgnored());
EXPECT_EQ(nullptr, obj_a->PreviousSiblingIncludingIgnored());
EXPECT_EQ(nullptr, obj_a->UnignoredPreviousSibling());
EXPECT_EQ(obj_ignored, obj_a->NextSiblingIncludingIgnored());
EXPECT_EQ(obj_b, obj_a->UnignoredNextSibling());
EXPECT_EQ(body, obj_a->PreviousInPreOrderIncludingIgnored());
EXPECT_EQ(root, obj_a->UnignoredPreviousInPreOrder());
EXPECT_EQ(obj_a_text, obj_a->NextInPreOrderIncludingIgnored());
EXPECT_EQ(obj_a_text, obj_a->UnignoredNextInPreOrder());
EXPECT_EQ(nullptr, obj_b->PreviousSiblingIncludingIgnored());
EXPECT_EQ(obj_a, obj_b->UnignoredPreviousSibling());
EXPECT_EQ(nullptr, obj_b->NextSiblingIncludingIgnored());
EXPECT_EQ(obj_c, obj_b->UnignoredNextSibling());
EXPECT_EQ(obj_ignored, obj_b->PreviousInPreOrderIncludingIgnored());
EXPECT_EQ(obj_a_text, obj_b->UnignoredPreviousInPreOrder()->ParentObject());
EXPECT_EQ(obj_b_text, obj_b->NextInPreOrderIncludingIgnored());
EXPECT_EQ(obj_b_text, obj_b->UnignoredNextInPreOrder());
EXPECT_EQ(obj_ignored, obj_c->PreviousSiblingIncludingIgnored());
EXPECT_EQ(obj_b, obj_c->UnignoredPreviousSibling());
EXPECT_EQ(nullptr, obj_c->NextSiblingIncludingIgnored());
EXPECT_EQ(nullptr, obj_c->UnignoredNextSibling());
EXPECT_EQ(
obj_b_text,
obj_c->PreviousInPreOrderIncludingIgnored()->ParentObjectUnignored());
EXPECT_EQ(obj_b_text,
obj_c->UnignoredPreviousInPreOrder()->ParentObjectUnignored());
EXPECT_EQ(obj_c_text, obj_c->NextInPreOrderIncludingIgnored());
EXPECT_EQ(obj_c_text, obj_c->UnignoredNextInPreOrder());
}
TEST_F(AccessibilityTest, TreeNavigationWithContinuations) {
// Continuations found in the layout tree should not appear in the
// accessibility tree. For example, the following accessibility tree should
// result from the following HTML.
//
// WebArea
// ++HTMLElement
// ++++BodyElement
// ++++++Link
// ++++++++StaticText "Before block element."
// ++++++++GenericContainer
// ++++++++++Paragraph
// ++++++++++++StaticText "Inside block element."
// ++++++++StaticText "After block element."
SetBodyInnerHTML(R"HTML(
<a id="link" href="#">
Before block element.
<div id="div">
<p id="paragraph">
Inside block element.
</p>
</div>
After block element.
</a>
)HTML");
const AXObject* ax_root = GetAXRootObject();
ASSERT_NE(nullptr, ax_root);
const AXObject* ax_body = GetAXBodyObject();
ASSERT_NE(nullptr, ax_body);
const AXObject* ax_link = GetAXObjectByElementId("link");
ASSERT_NE(nullptr, ax_link);
const AXObject* ax_text_before = ax_link->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text_before);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_before->RoleValue());
ASSERT_FALSE(ax_text_before->AccessibilityIsIgnored());
const AXObject* ax_div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, ax_div);
const AXObject* ax_paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, ax_paragraph);
const AXObject* ax_text_inside = ax_paragraph->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text_inside);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_inside->RoleValue());
ASSERT_FALSE(ax_text_inside->AccessibilityIsIgnored());
const AXObject* ax_text_after = ax_link->LastChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text_after);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_after->RoleValue());
ASSERT_FALSE(ax_text_after->AccessibilityIsIgnored());
//
// Test parent / child relationships individually. This is easier to debug
// than printing the whole accessibility tree as a string and comparing with
// an expected tree.
//
// BlockInInline changes |ax_body| not to be ignored. See the design doc at
// crbug.com/716930 for more details.
EXPECT_EQ(ax_body, ax_link->ParentObjectUnignored());
EXPECT_EQ(ax_body, ax_link->ParentObjectIncludedInTree());
EXPECT_EQ(ax_link, ax_text_before->ParentObjectUnignored());
EXPECT_EQ(ax_link, ax_text_before->ParentObjectIncludedInTree());
EXPECT_EQ(ax_link, ax_div->ParentObjectUnignored());
EXPECT_EQ(ax_link, ax_div->ParentObjectIncludedInTree());
EXPECT_EQ(ax_link, ax_text_after->ParentObjectUnignored());
EXPECT_EQ(ax_link, ax_text_after->ParentObjectIncludedInTree());
EXPECT_EQ(ax_div, ax_link->ChildAtIncludingIgnored(1));
EXPECT_EQ(ax_div, ax_link->UnignoredChildAt(1));
EXPECT_EQ(nullptr, ax_text_before->PreviousSiblingIncludingIgnored());
EXPECT_EQ(nullptr, ax_text_before->UnignoredPreviousSibling());
EXPECT_EQ(ax_div, ax_text_before->NextSiblingIncludingIgnored());
EXPECT_EQ(ax_div, ax_text_before->UnignoredNextSibling());
EXPECT_EQ(ax_div, ax_text_after->PreviousSiblingIncludingIgnored());
EXPECT_EQ(ax_div, ax_text_after->UnignoredPreviousSibling());
EXPECT_EQ(nullptr, ax_text_after->NextSiblingIncludingIgnored());
EXPECT_EQ(nullptr, ax_text_after->UnignoredNextSibling());
EXPECT_EQ(ax_paragraph, ax_div->ChildAtIncludingIgnored(0));
EXPECT_EQ(ax_paragraph, ax_div->UnignoredChildAt(0));
EXPECT_EQ(ax_div, ax_paragraph->ParentObjectUnignored());
EXPECT_EQ(ax_div, ax_paragraph->ParentObjectIncludedInTree());
EXPECT_EQ(ax_paragraph, ax_text_inside->ParentObjectUnignored());
EXPECT_EQ(ax_paragraph, ax_text_inside->ParentObjectIncludedInTree());
}
TEST_F(AccessibilityTest, TreeNavigationWithInlineTextBoxes) {
SetBodyInnerHTML(R"HTML(
Before paragraph element.
<p id="paragraph">
Inside paragraph element.
</p>
After paragraph element.
)HTML");
AXObject* ax_root = GetAXRootObject();
ASSERT_NE(nullptr, ax_root);
ax_root->LoadInlineTextBoxes();
const AXObject* ax_paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, ax_paragraph);
const AXObject* ax_text_inside = ax_paragraph->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, ax_text_inside);
ASSERT_EQ(ax::mojom::Role::kStaticText, ax_text_inside->RoleValue());
const AXObject* ax_text_before = ax_paragraph->UnignoredPreviousSibling();
ASSERT_NE(nullptr, ax_text_before);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, ax_text_before->RoleValue());
const AXObject* ax_text_after = ax_paragraph->UnignoredNextSibling();
ASSERT_NE(nullptr, ax_text_after);
ASSERT_EQ(ax::mojom::blink::Role::kStaticText, ax_text_after->RoleValue());
//
// Verify parent / child relationships between static text and inline text
// boxes.
//
EXPECT_EQ(1, ax_text_before->ChildCountIncludingIgnored());
EXPECT_EQ(1, ax_text_before->UnignoredChildCount());
const AXObject* ax_inline_before =
ax_text_before->FirstChildIncludingIgnored();
EXPECT_EQ(ax::mojom::blink::Role::kInlineTextBox,
ax_inline_before->RoleValue());
EXPECT_EQ(ax_text_before, ax_inline_before->ParentObjectIncludedInTree());
EXPECT_EQ(ax_text_before, ax_inline_before->ParentObjectUnignored());
EXPECT_EQ(1, ax_text_inside->ChildCountIncludingIgnored());
EXPECT_EQ(1, ax_text_inside->UnignoredChildCount());
const AXObject* ax_inline_inside =
ax_text_inside->FirstChildIncludingIgnored();
EXPECT_EQ(ax::mojom::blink::Role::kInlineTextBox,
ax_inline_inside->RoleValue());
EXPECT_EQ(ax_text_inside, ax_inline_inside->ParentObjectIncludedInTree());
EXPECT_EQ(ax_text_inside, ax_inline_inside->ParentObjectUnignored());
EXPECT_EQ(1, ax_text_after->ChildCountIncludingIgnored());
EXPECT_EQ(1, ax_text_after->UnignoredChildCount());
const AXObject* ax_inline_after = ax_text_after->FirstChildIncludingIgnored();
EXPECT_EQ(ax::mojom::blink::Role::kInlineTextBox,
ax_inline_after->RoleValue());
EXPECT_EQ(ax_text_after, ax_inline_after->ParentObjectIncludedInTree());
EXPECT_EQ(ax_text_after, ax_inline_after->ParentObjectUnignored());
}
TEST_F(AccessibilityTest, AXObjectComparisonOperators) {
SetBodyInnerHTML(R"HTML(<input id="input" type="text" value="value">
<p id="paragraph">hello<br id="br">there</p>
<button id="button">button</button>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* input = GetAXObjectByElementId("input");
ASSERT_NE(nullptr, input);
const AXObject* paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, paragraph);
const AXObject* br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, br);
const AXObject* button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
EXPECT_TRUE(*root == *root);
EXPECT_FALSE(*root != *root);
EXPECT_FALSE(*root < *root);
EXPECT_TRUE(*root <= *root);
EXPECT_FALSE(*root > *root);
EXPECT_TRUE(*root >= *root);
EXPECT_TRUE(*input > *root);
EXPECT_TRUE(*input >= *root);
EXPECT_FALSE(*input < *root);
EXPECT_FALSE(*input <= *root);
EXPECT_TRUE(*input != *root);
EXPECT_TRUE(*input < *paragraph);
EXPECT_TRUE(*br > *input);
EXPECT_TRUE(*paragraph < *br);
EXPECT_TRUE(*br >= *paragraph);
EXPECT_TRUE(*paragraph < *button);
EXPECT_TRUE(*button > *br);
EXPECT_FALSE(*button < *button);
EXPECT_TRUE(*button <= *button);
EXPECT_TRUE(*button >= *button);
EXPECT_FALSE(*button > *button);
}
TEST_F(AccessibilityTest, AXObjectUnignoredAncestorsIterator) {
SetBodyInnerHTML(
R"HTML(<p id="paragraph"><b role="none" id="bold"><br id="br"></b></p>)HTML");
AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
AXObject* paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, paragraph);
AXObject* bold = GetAXObjectByElementId("bold");
ASSERT_NE(nullptr, bold);
AXObject* br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, br);
ASSERT_EQ(ax::mojom::Role::kLineBreak, br->RoleValue());
AXObject::AncestorsIterator iter = br->UnignoredAncestorsBegin();
EXPECT_EQ(*paragraph, *iter);
EXPECT_EQ(ax::mojom::Role::kParagraph, iter->RoleValue());
EXPECT_EQ(*root, *++iter);
EXPECT_EQ(*root, *iter++);
EXPECT_EQ(br->UnignoredAncestorsEnd(), ++iter);
}
TEST_F(AccessibilityTest, AxNodeObjectContainsHtmlAnchorElementUrl) {
SetBodyInnerHTML(R"HTML(<a id="anchor" href="http://test.com">link</a>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* anchor = GetAXObjectByElementId("anchor");
ASSERT_NE(nullptr, anchor);
// Passing a malformed string to KURL returns an empty URL, so verify the
// AXObject's URL is non-empty first to catch errors in the test itself.
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL("http://test.com"));
}
TEST_F(AccessibilityTest, AxNodeObjectContainsSvgAnchorElementUrl) {
SetBodyInnerHTML(R"HTML(
<svg>
<a id="anchor" xlink:href="http://test.com"></a>
</svg>
)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* anchor = GetAXObjectByElementId("anchor");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL("http://test.com"));
}
TEST_F(AccessibilityTest, AxNodeObjectContainsImageUrl) {
SetBodyInnerHTML(R"HTML(<img id="anchor" src="http://test.png" />)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* anchor = GetAXObjectByElementId("anchor");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL("http://test.png"));
}
TEST_F(AccessibilityTest, AxNodeObjectContainsInPageLinkTarget) {
GetDocument().SetBaseURLOverride(KURL("http://test.com"));
SetBodyInnerHTML(R"HTML(<a id="anchor" href="#target">link</a>)HTML");
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* anchor = GetAXObjectByElementId("anchor");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL("http://test.com/#target"));
}
TEST_F(AccessibilityTest, AxNodeObjectInPageLinkTargetNonAscii) {
GetDocument().SetURL(KURL("http://test.com"));
// ö is U+00F6 which URI encodes to %C3%B6
//
// This file is forced to be UTF-8 by the build system,
// the uR"" will create char16_t[] of UTF-16,
// WTF::String will wrap the char16_t* as UTF-16.
// All this is checked by ensuring a match against u"\u00F6".
//
// TODO(1117212): The escaped version currently takes precedence.
// <h1 id="%C3%B6">O2</h1>
SetBodyInnerHTML(
uR"HTML(
<a href="#ö" id="anchor">O</a>
<h1 id="ö">O</h1>"
<a href="#t%6Fp" id="top_test">top</a>"
<a href="#" id="empty_test">also top</a>");
)HTML");
{
// anchor
const AXObject* anchor = GetAXObjectByElementId("anchor");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL(u"http://test.com/#\u00F6"));
const AXObject* target = anchor->InPageLinkTarget();
ASSERT_NE(nullptr, target);
auto* targetElement = DynamicTo<Element>(target->GetNode());
ASSERT_NE(nullptr, target);
ASSERT_TRUE(targetElement->HasID());
EXPECT_EQ(targetElement->IdForStyleResolution(), String(u"\u00F6"));
}
{
// top_test
const AXObject* anchor = GetAXObjectByElementId("top_test");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL(u"http://test.com/#t%6Fp"));
const AXObject* target = anchor->InPageLinkTarget();
ASSERT_NE(nullptr, target);
EXPECT_EQ(&GetDocument(), target->GetNode());
}
{
// empty_test
const AXObject* anchor = GetAXObjectByElementId("empty_test");
ASSERT_NE(nullptr, anchor);
EXPECT_FALSE(anchor->Url().IsEmpty());
EXPECT_EQ(anchor->Url(), KURL(u"http://test.com/#"));
const AXObject* target = anchor->InPageLinkTarget();
ASSERT_NE(nullptr, target);
EXPECT_EQ(&GetDocument(), target->GetNode());
}
}
TEST_F(AccessibilityTest, NextOnLine) {
SetBodyInnerHTML(R"HTML(
<style>
html {
font-size: 10px;
}
/* TODO(kojii): |NextOnLine| doesn't work for culled-inline.
Ensure spans are not culled to avoid hitting the case. */
span {
background: gray;
}
</style>
<div><span id="span1">a</span><span>b</span></div>
)HTML");
const AXObject* span1 = GetAXObjectByElementId("span1");
ASSERT_NE(nullptr, span1);
const AXObject* next = span1->NextOnLine();
ASSERT_NE(nullptr, next);
EXPECT_EQ("b", next->GetNode()->textContent());
}
TEST_F(AccessibilityTest, NextOnLineInlineBlock) {
// Note the spans must be in the same line or we could get other unwanted
// behavior. See https://crbug.com/1511390 for details.
SetBodyInnerHTML(R"HTML(
<div contenteditable="true" style="outline: 1px solid;">
<div>first line</div>
<span id="this">this line </span><span style="display: inline-block"><span style="display: block;">is</span></span><span> broken.</span>
<div>last line</div>
</div>
)HTML");
const AXObject* this_object = GetAXObjectByElementId("this");
ASSERT_NE(nullptr, this_object);
const AXObject* next = this_object->NextOnLine();
ASSERT_NE(nullptr, next);
EXPECT_EQ("is", next->GetNode()->textContent());
next = next->NextOnLine();
ASSERT_NE(nullptr, next);
EXPECT_EQ(" broken.", next->GetNode()->textContent());
AXObject* prev = next->PreviousOnLine();
ASSERT_NE(nullptr, prev);
EXPECT_EQ("is", prev->GetNode()->textContent());
prev = prev->PreviousOnLine();
ASSERT_NE(nullptr, prev);
EXPECT_EQ("this line ", prev->GetNode()->textContent());
}
TEST_F(AccessibilityTest, NextAndPreviousOnLineInert) {
// Spans need to be in the same line: see https://crbug.com/1511390.
SetBodyInnerHTML(R"HTML(
<div>
<div>first line</div>
<span id="span1">go </span><span inert>inert1</span><span inert>inert2</span><span>blue</span>
<div>last line</div>
</div>
)HTML");
const AXObject* span1 = GetAXObjectByElementId("span1");
ASSERT_NE(nullptr, span1);
EXPECT_EQ("go ", span1->GetNode()->textContent());
const AXObject* next = span1->NextOnLine();
ASSERT_NE(nullptr, next);
EXPECT_EQ("blue", next->GetNode()->textContent());
// Now we go backwards.
const AXObject* previous = next->PreviousOnLine();
EXPECT_EQ("go ", previous->GetNode()->textContent());
}
TEST_F(AccessibilityTest, NextOnLineAriaHidden) {
// Note the spans must be in the same line or we could get other unwanted
// behavior. See https://crbug.com/1511390 for details.
SetBodyInnerHTML(R"HTML(
<div contenteditable="true" style="outline: 1px solid;">
<div>first line</div>
<span id="this">this line </span><span aria-hidden="true">is</span><span> broken.</span>
<div>last line</div>
</div>
)HTML");
const AXObject* this_object = GetAXObjectByElementId("this");
ASSERT_NE(nullptr, this_object);
const AXObject* next = this_object->NextOnLine();
ASSERT_NE(nullptr, next);
EXPECT_EQ(" broken.", next->GetNode()->textContent());
const AXObject* prev = next->PreviousOnLine();
ASSERT_NE(nullptr, prev);
EXPECT_EQ("this line ", prev->GetNode()->textContent());
}
TEST_F(AccessibilityTest, TableRowAndCellIsLineBreakingObject) {
SetBodyInnerHTML(R"HTML(
<table id="table">
<caption>Caption</caption>
<tr id="row">
<td id="cell">Cell</td>
</tr>
</table>
)HTML");
const AXObject* table = GetAXObjectByElementId("table");
ASSERT_NE(nullptr, table);
ASSERT_EQ(ax::mojom::Role::kTable, table->RoleValue());
EXPECT_TRUE(table->IsLineBreakingObject());
const AXObject* row = GetAXObjectByElementId("row");
ASSERT_NE(nullptr, row);
ASSERT_EQ(ax::mojom::Role::kRow, row->RoleValue());
EXPECT_TRUE(row->IsLineBreakingObject());
const AXObject* cell = GetAXObjectByElementId("cell");
ASSERT_NE(nullptr, cell);
ASSERT_EQ(ax::mojom::Role::kCell, cell->RoleValue());
EXPECT_TRUE(cell->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, TestSetRangeValueVideoControlSlider) {
SetBodyInnerHTML(R"HTML(
<body>
<video id="vid" src="bear.webm"></video>
</body>
)HTML");
AXObject* video = GetAXObjectByElementId("vid");
Node* video_node = video->GetNode();
ASSERT_NE(nullptr, video_node);
auto* video_element = DynamicTo<HTMLMediaElement>(video_node);
ASSERT_NE(nullptr, video_node);
Node* timeline_node =
video_element->GetMediaControls()->TimelineLayoutObject()->GetNode();
ASSERT_NE(nullptr, timeline_node);
AXObjectCache* cache = timeline_node->GetDocument().ExistingAXObjectCache();
ASSERT_NE(nullptr, cache);
AXObject* video_slider = cache->ObjectFromAXID(timeline_node->GetDomNodeId());
ASSERT_NE(nullptr, video_slider);
ASSERT_EQ(video_slider->RoleValue(), ax::mojom::blink::Role::kSlider);
float value = 0.0f;
EXPECT_TRUE(video_slider->ValueForRange(&value));
EXPECT_EQ(0.0f, value);
std::string value_to_set("1.0");
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kSetValue;
action_data.value = value_to_set;
action_data.target_node_id = video_slider->AXObjectID();
EXPECT_TRUE(video_slider->PerformAction(action_data));
EXPECT_TRUE(video_slider->ValueForRange(&value));
EXPECT_EQ(1.0f, value);
}
TEST_F(AccessibilityTest,
PreservedWhitespaceWithInitialLineBreakIsLineBreakingObject) {
SetBodyInnerHTML(R"HTML(
<span style="white-space: pre-line" id="preserved">
First Paragraph
Second Paragraph
Third Paragraph
</span>)HTML");
const AXObject* preserved_span = GetAXObjectByElementId("preserved");
ASSERT_NE(nullptr, preserved_span);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, preserved_span->RoleValue());
ASSERT_EQ(1, preserved_span->UnignoredChildCount());
EXPECT_FALSE(preserved_span->IsLineBreakingObject());
AXObject* preserved_text = preserved_span->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, preserved_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, preserved_text->RoleValue());
EXPECT_TRUE(preserved_text->IsLineBreakingObject())
<< "This text node starts with a line break character, so it should be a "
"paragraph boundary.";
// Expect 7 kInlineTextBox children.
// 3 lines of text, and 4 newlines including one a the start of the text.
preserved_text->LoadInlineTextBoxes();
ASSERT_EQ(7, preserved_text->UnignoredChildCount());
ASSERT_THAT(preserved_text->UnignoredChildren(),
Each(SafeMatcherCast<AXObject*>(
Property("AXObject::RoleValue()", &AXObject::RoleValue,
ax::mojom::Role::kInlineTextBox))));
ASSERT_EQ(preserved_text->UnignoredChildAt(0)->ComputedName(), "\n");
EXPECT_TRUE(preserved_text->UnignoredChildAt(0)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(1)->ComputedName(),
"First Paragraph");
EXPECT_FALSE(preserved_text->UnignoredChildAt(1)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(2)->ComputedName(), "\n");
EXPECT_TRUE(preserved_text->UnignoredChildAt(2)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(3)->ComputedName(),
"Second Paragraph");
EXPECT_FALSE(preserved_text->UnignoredChildAt(3)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(4)->ComputedName(), "\n");
EXPECT_TRUE(preserved_text->UnignoredChildAt(4)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(5)->ComputedName(),
"Third Paragraph");
EXPECT_FALSE(preserved_text->UnignoredChildAt(5)->IsLineBreakingObject());
ASSERT_EQ(preserved_text->UnignoredChildAt(6)->ComputedName(), "\n");
EXPECT_TRUE(preserved_text->UnignoredChildAt(6)->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, DivWithFirstLetterIsLineBreakingObject) {
SetBodyInnerHTML(R"HTML(
<style>div::first-letter { color: "red"; }</style>
<div id="firstLetter">First letter</div>)HTML");
const AXObject* div = GetAXObjectByElementId("firstLetter");
ASSERT_NE(nullptr, div);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, div->RoleValue());
ASSERT_EQ(1, div->UnignoredChildCount());
EXPECT_TRUE(div->IsLineBreakingObject());
AXObject* div_text = div->FirstChildIncludingIgnored();
ASSERT_NE(nullptr, div_text);
ASSERT_EQ(ax::mojom::Role::kStaticText, div_text->RoleValue());
EXPECT_FALSE(div_text->IsLineBreakingObject());
div_text->LoadInlineTextBoxes();
ASSERT_EQ(1, div_text->UnignoredChildCount());
ASSERT_EQ(ax::mojom::Role::kInlineTextBox,
div_text->UnignoredChildAt(0)->RoleValue());
ASSERT_EQ(div_text->UnignoredChildAt(0)->ComputedName(), "First letter");
EXPECT_FALSE(div_text->UnignoredChildAt(0)->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, SlotIsLineBreakingObject) {
// Even though a <span>, <b> and <i> element are not line-breaking, a
// paragraph element in the shadow DOM should be.
const char* body_content = R"HTML(
<span id="host">
<b slot="slot1" id="slot1">slot1</b>
<i slot="slot2" id="slot2">slot2</i>
</span>)HTML";
const char* shadow_content = R"HTML(
<p><slot name="slot1"></slot></p>
<p><slot name="slot2"></slot></p>
)HTML";
SetBodyContent(body_content);
ShadowRoot& shadow_root =
GetElementById("host")->AttachShadowRootForTesting(ShadowRootMode::kOpen);
shadow_root.setInnerHTML(String::FromUTF8(shadow_content),
ASSERT_NO_EXCEPTION);
UpdateAllLifecyclePhasesForTest();
const AXObject* host = GetAXObjectByElementId("host");
ASSERT_NE(nullptr, host);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, host->RoleValue());
EXPECT_FALSE(host->IsLineBreakingObject());
EXPECT_TRUE(host->ParentObjectUnignored()->IsLineBreakingObject());
const AXObject* slot1 = GetAXObjectByElementId("slot1");
ASSERT_NE(nullptr, slot1);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, slot1->RoleValue());
EXPECT_FALSE(slot1->IsLineBreakingObject());
EXPECT_TRUE(slot1->ParentObjectUnignored()->IsLineBreakingObject());
const AXObject* slot2 = GetAXObjectByElementId("slot2");
ASSERT_NE(nullptr, slot2);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, slot2->RoleValue());
EXPECT_FALSE(slot2->IsLineBreakingObject());
EXPECT_TRUE(slot2->ParentObjectUnignored()->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, LineBreakInDisplayLockedIsLineBreakingObject) {
SetBodyInnerHTML(R"HTML(
<div id="spacer"
style="height: 30000px; contain-intrinsic-size: 1px 30000px;"></div>
<p id="lockedContainer" style="content-visibility: auto">
Line 1
<br id="br" style="content-visibility: hidden">
Line 2
</p>
)HTML");
const AXObject* paragraph = GetAXObjectByElementId("lockedContainer");
ASSERT_NE(nullptr, paragraph);
ASSERT_EQ(ax::mojom::Role::kParagraph, paragraph->RoleValue());
ASSERT_EQ(3, paragraph->UnignoredChildCount());
ASSERT_EQ(paragraph->GetNode(),
DisplayLockUtilities::LockedInclusiveAncestorPreventingPaint(
*paragraph->GetNode()))
<< "The <p> element should be display locked.";
EXPECT_TRUE(paragraph->IsLineBreakingObject());
const AXObject* br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, br);
ASSERT_EQ(ax::mojom::Role::kGenericContainer, br->RoleValue())
<< "The <br> child should be display locked and thus have a generic "
"role.";
ASSERT_EQ(paragraph->GetNode(),
DisplayLockUtilities::LockedInclusiveAncestorPreventingPaint(
*br->GetNode()))
<< "The <br> child should be display locked.";
EXPECT_TRUE(br->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, ListMarkerIsNotLineBreakingObject) {
SetBodyInnerHTML(R"HTML(
<style>
ul li::marker {
content: "X";
}
</style>
<ul id="unorderedList">
<li id="unorderedListItem">.....
Unordered item 1
</li>
</ul>
<ol id="orderedList">
<li id="orderedListItem">
Ordered item 1
</li>
</ol>
)HTML");
const AXObject* unordered_list = GetAXObjectByElementId("unorderedList");
ASSERT_NE(nullptr, unordered_list);
ASSERT_EQ(ax::mojom::Role::kList, unordered_list->RoleValue());
EXPECT_TRUE(unordered_list->IsLineBreakingObject());
const AXObject* unordered_list_item =
GetAXObjectByElementId("unorderedListItem");
ASSERT_NE(nullptr, unordered_list_item);
ASSERT_EQ(ax::mojom::Role::kListItem, unordered_list_item->RoleValue());
EXPECT_TRUE(unordered_list_item->IsLineBreakingObject());
const AXObject* unordered_list_marker =
unordered_list_item->UnignoredChildAt(0);
ASSERT_NE(nullptr, unordered_list_marker);
ASSERT_EQ(ax::mojom::Role::kListMarker, unordered_list_marker->RoleValue());
EXPECT_FALSE(unordered_list_marker->IsLineBreakingObject());
const AXObject* ordered_list = GetAXObjectByElementId("orderedList");
ASSERT_NE(nullptr, ordered_list);
ASSERT_EQ(ax::mojom::Role::kList, ordered_list->RoleValue());
EXPECT_TRUE(ordered_list->IsLineBreakingObject());
const AXObject* ordered_list_item = GetAXObjectByElementId("orderedListItem");
ASSERT_NE(nullptr, ordered_list_item);
ASSERT_EQ(ax::mojom::Role::kListItem, ordered_list_item->RoleValue());
EXPECT_TRUE(ordered_list_item->IsLineBreakingObject());
const AXObject* ordered_list_marker = ordered_list_item->UnignoredChildAt(0);
ASSERT_NE(nullptr, ordered_list_marker);
ASSERT_EQ(ax::mojom::Role::kListMarker, ordered_list_marker->RoleValue());
EXPECT_FALSE(ordered_list_marker->IsLineBreakingObject());
}
TEST_F(AccessibilityTest, CheckNoDuplicateChildren) {
// Clear inline text boxes and refresh the tree.
ui::AXMode mode(ui::kAXModeComplete);
mode.set_mode(ui::AXMode::kInlineTextBoxes, false);
ax_context_->SetAXMode(mode);
GetAXObjectCache().MarkDocumentDirty();
GetAXObjectCache().UpdateAXForAllDocuments();
SetBodyInnerHTML(R"HTML(
<select id="sel"><option>1</option></select>
)HTML");
AXObject* ax_select = GetAXObjectByElementId("sel");
ax_select->SetNeedsToUpdateChildren();
GetAXObjectCache().UpdateAXForAllDocuments();
ASSERT_EQ(
ax_select->FirstChildIncludingIgnored()->ChildCountIncludingIgnored(), 1);
}
TEST_F(AccessibilityTest, InitRelationCacheLabelFor) {
// Most other tests already have accessibility initialized
// first, but we don't want to in this test.
//
// Get rid of the AXContext so the AXObjectCache is destroyed.
ax_context_.reset(nullptr);
SetBodyInnerHTML(R"HTML(
<label for="a"></label>
<input id="a">
<input id="b">
)HTML");
// Now recreate an AXContext, simulating what happens if accessibility
// is enabled after the document is loaded.
ax_context_ = std::make_unique<AXContext>(GetDocument(), ui::kAXModeComplete);
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
const AXObject* input_a = GetAXObjectByElementId("a");
ASSERT_NE(nullptr, input_a);
const AXObject* input_b = GetAXObjectByElementId("b");
ASSERT_NE(nullptr, input_b);
}
TEST_F(AccessibilityTest, InitRelationCacheAriaOwns) {
// Most other tests already have accessibility initialized
// first, but we don't want to in this test.
//
// Get rid of the AXContext so the AXObjectCache is destroyed.
ax_context_.reset(nullptr);
SetBodyInnerHTML(R"HTML(
<ul id="ul" aria-owns="li"></ul>
<div role="section" id="div">
<li id="li"></li>
</div>
)HTML");
// Now recreate an AXContext, simulating what happens if accessibility
// is enabled after the document is loaded.
ax_context_ = std::make_unique<AXContext>(GetDocument(), ui::kAXModeComplete);
const AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
// Note: retrieve the LI first and check that its parent is not
// the paragraph element. If we were to retrieve the UL element,
// that would trigger the aria-owns check and wouln't allow us to
// test whether the relation cache was initialized.
const AXObject* li = GetAXObjectByElementId("li");
ASSERT_NE(nullptr, li);
const AXObject* div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, div);
EXPECT_NE(li->ParentObjectUnignored(), div);
const AXObject* ul = GetAXObjectByElementId("ul");
ASSERT_NE(nullptr, ul);
EXPECT_EQ(li->ParentObjectUnignored(), ul);
}
TEST_F(AccessibilityTest, IsSelectedFromFocusSupported) {
SetBodyInnerHTML(R"HTML(
<input role="combobox" type="search" aria-expanded="true"
aria-haspopup="true" aria-autocomplete="list1" aria-owns="list1">
<ul id="list1" role="listbox">
<li id="option1" role="option" tabindex="-1">Apple</li>
</ul>
<input role="combobox" type="search" aria-expanded="true"
aria-haspopup="true" aria-autocomplete="list2" aria-owns="list2">
<ul id="list2" role="listbox">
<li id="option2" role="row" tabindex="-1">Apple</li>
</ul>
<input role="combobox" type="search" aria-expanded="true"
aria-haspopup="true" aria-autocomplete="list3" aria-owns="list3">
<ul id="list3" role="listbox">
<li id="option3" role="option" tabindex="-1"
aria-selected="false">Apple</li>
</ul>
<input role="combobox" type="search" aria-expanded="true"
aria-haspopup="true" aria-autocomplete="list4" aria-owns="list4">
<ul id="list4" role="listbox">
<li id="option4" role="option" tabindex="-1"
aria-selected="true">Apple</li>
<li id="option5" role="option" tabindex="-1">Orange</li>
</ul>
)HTML");
const AXObject* option1 = GetAXObjectByElementId("option1");
ASSERT_NE(option1, nullptr);
const AXObject* option2 = GetAXObjectByElementId("option2");
ASSERT_NE(option2, nullptr);
const AXObject* option3 = GetAXObjectByElementId("option3");
ASSERT_NE(option3, nullptr);
const AXObject* option4 = GetAXObjectByElementId("option4");
ASSERT_NE(option4, nullptr);
const AXObject* option5 = GetAXObjectByElementId("option5");
ASSERT_NE(option5, nullptr);
EXPECT_TRUE(option1->IsSelectedFromFocusSupported());
EXPECT_FALSE(option2->IsSelectedFromFocusSupported());
EXPECT_FALSE(option3->IsSelectedFromFocusSupported());
EXPECT_FALSE(option4->IsSelectedFromFocusSupported());
// TODO(crbug.com/1143451): #option5 should not support selection from focus
// because #option4 is explicitly selected.
EXPECT_TRUE(option5->IsSelectedFromFocusSupported());
}
TEST_F(AccessibilityTest, GetBoundsInFrameCoordinatesSvgText) {
SetBodyInnerHTML(R"HTML(
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
<text id="t1" x="100">Text1</text>
<text id="t2" x="50">Text1</text>
</svg>)HTML");
AXObject* text1 = GetAXObjectByElementId("t1");
ASSERT_NE(text1, nullptr);
AXObject* text2 = GetAXObjectByElementId("t2");
ASSERT_NE(text2, nullptr);
PhysicalRect bounds1 = text1->GetBoundsInFrameCoordinates();
PhysicalRect bounds2 = text2->GetBoundsInFrameCoordinates();
// Check if bounding boxes for SVG <text> respect to positioning
// attributes such as 'x'.
EXPECT_GT(bounds1.X(), bounds2.X());
}
TEST_F(AccessibilityTest, ComputeIsInertReason) {
NonThrowableExceptionState exception_state;
SetBodyInnerHTML(R"HTML(
<div id="div1" inert>inert</div>
<dialog id="dialog1">dialog</dialog>
<dialog id="dialog2" inert>inert dialog</dialog>
<p id="p1">fullscreen</p>
<p id="p2" inert>inert fullscreen</p>
)HTML");
Document& document = GetDocument();
Element* body = document.body();
Element* div1 = GetElementById("div1");
Node* div1_text = div1->firstChild();
auto* dialog1 = To<HTMLDialogElement>(GetElementById("dialog1"));
Node* dialog1_text = dialog1->firstChild();
auto* dialog2 = To<HTMLDialogElement>(GetElementById("dialog2"));
Node* dialog2_text = dialog2->firstChild();
Element* p1 = GetElementById("p1");
Node* p1_text = p1->firstChild();
Element* p2 = GetElementById("p2");
Node* p2_text = p2->firstChild();
auto AssertInertReasons = [&](Node* node, AXIgnoredReason expectation) {
AXObject* object = GetAXObjectCache().Get(node);
ASSERT_NE(object, nullptr);
AXObject::IgnoredReasons reasons;
ASSERT_TRUE(object->ComputeIsInert(&reasons));
ASSERT_EQ(reasons.size(), 1u);
ASSERT_EQ(reasons[0].reason, expectation);
};
auto AssertNotInert = [&](Node* node) {
AXObject* object = GetAXObjectCache().Get(node);
ASSERT_NE(object, nullptr);
AXObject::IgnoredReasons reasons;
ASSERT_FALSE(object->ComputeIsInert(&reasons));
ASSERT_EQ(reasons.size(), 0u);
};
auto EnterFullscreen = [&](Element* element) {
LocalFrame::NotifyUserActivation(
document.GetFrame(), mojom::UserActivationNotificationType::kTest);
Fullscreen::RequestFullscreen(*element);
Fullscreen::DidResolveEnterFullscreenRequest(document, /*granted*/ true);
};
auto ExitFullscreen = [&]() {
Fullscreen::FullyExitFullscreen(document);
Fullscreen::DidExitFullscreen(document);
};
AssertNotInert(body);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertNotInert(dialog1);
AssertNotInert(dialog1_text);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertNotInert(p1);
AssertNotInert(p1_text);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
dialog1->showModal(exception_state);
AssertInertReasons(body, kAXActiveModalDialog);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertNotInert(dialog1);
AssertNotInert(dialog1_text);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertInertReasons(p1, kAXActiveModalDialog);
AssertInertReasons(p1_text, kAXActiveModalDialog);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
dialog2->showModal(exception_state);
AssertInertReasons(body, kAXActiveModalDialog);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertInertReasons(dialog1, kAXActiveModalDialog);
AssertInertReasons(dialog1_text, kAXActiveModalDialog);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertInertReasons(p1, kAXActiveModalDialog);
AssertInertReasons(p1_text, kAXActiveModalDialog);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
EnterFullscreen(p1);
AssertInertReasons(body, kAXActiveModalDialog);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertInertReasons(dialog1, kAXActiveModalDialog);
AssertInertReasons(dialog1_text, kAXActiveModalDialog);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertInertReasons(p1, kAXActiveModalDialog);
AssertInertReasons(p1_text, kAXActiveModalDialog);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
dialog1->close();
dialog2->close();
AssertInertReasons(body, kAXActiveFullscreenElement);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertInertReasons(dialog1, kAXActiveFullscreenElement);
AssertInertReasons(dialog1_text, kAXActiveFullscreenElement);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertNotInert(p1);
AssertNotInert(p1_text);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
ExitFullscreen();
EnterFullscreen(p2);
AssertInertReasons(body, kAXActiveFullscreenElement);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertInertReasons(dialog1, kAXActiveFullscreenElement);
AssertInertReasons(dialog1_text, kAXActiveFullscreenElement);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertInertReasons(p1, kAXActiveFullscreenElement);
AssertInertReasons(p1_text, kAXActiveFullscreenElement);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
ExitFullscreen();
AssertNotInert(body);
AssertInertReasons(div1, kAXInertElement);
AssertInertReasons(div1_text, kAXInertSubtree);
AssertNotInert(dialog1);
AssertNotInert(dialog1_text);
AssertInertReasons(dialog2, kAXInertElement);
AssertInertReasons(dialog2_text, kAXInertSubtree);
AssertNotInert(p1);
AssertNotInert(p1_text);
AssertInertReasons(p2, kAXInertElement);
AssertInertReasons(p2_text, kAXInertSubtree);
}
TEST_F(AccessibilityTest, ComputeIsInertWithNonHTMLElements) {
SetBodyInnerHTML(R"HTML(
<main inert>
main
<foo inert>
foo
<svg inert>
foo
<foreignObject inert>
foo
<div inert>
div
<math inert>
div
<mi inert>
div
<span inert>
span
</span>
</mi>
</math>
</div>
</foreignObject>
</svg>
</foo>
</main>
)HTML");
Document& document = GetDocument();
Element* element = document.QuerySelector(AtomicString("main"));
while (element) {
Node* node = element->firstChild();
AXObject* ax_node = GetAXObjectCache().Get(node);
// The text indicates the expected inert root, which is the nearest HTML
// element ancestor with the 'inert' attribute.
AtomicString selector(node->textContent().Impl());
Element* inert_root = document.QuerySelector(selector);
AXObject* ax_inert_root = GetAXObjectCache().Get(inert_root);
AXObject::IgnoredReasons reasons;
ASSERT_TRUE(ax_node->ComputeIsInert(&reasons));
ASSERT_EQ(reasons.size(), 1u);
ASSERT_EQ(reasons[0].reason, kAXInertSubtree);
ASSERT_EQ(reasons[0].related_object.Get(), ax_inert_root);
element = ElementTraversal::FirstChild(*element);
}
}
TEST_F(AccessibilityTest, CanSetFocusInCanvasFallbackContent) {
SetBodyInnerHTML(R"HTML(
<canvas>
<section>
<div tabindex="-1" id="div"></div>
<span tabindex="-1" id="span"></div>
<a tabindex="-1" id="a"></a>
</section>
<section hidden>
<div tabindex="-1" id="div-hidden"></div>
<span tabindex="-1" id="span-hidden"></div>
<a tabindex="-1" id="a-hidden"></a>
</section>
<section inert>
<div tabindex="-1" id="div-inert"></div>
<span tabindex="-1" id="span-inert"></div>
<a tabindex="-1" id="a-inert"></a>
</section>
<section hidden inert>
<div tabindex="-1" id="div-hidden-inert"></div>
<span tabindex="-1" id="span-hidden-inert"></div>
<a tabindex="-1" id="a-hidden-inert"></a>
</section>
</div>
)HTML");
// Elements being used as relevant canvas fallback content can be focusable.
ASSERT_TRUE(GetAXObjectByElementId("div")->CanSetFocusAttribute());
ASSERT_TRUE(GetAXObjectByElementId("span")->CanSetFocusAttribute());
ASSERT_TRUE(GetAXObjectByElementId("a")->CanSetFocusAttribute());
// But they are not focusable if in a display:none subtree...
ASSERT_FALSE(GetAXObjectByElementId("div-hidden")->CanSetFocusAttribute());
ASSERT_FALSE(GetAXObjectByElementId("span-hidden")->CanSetFocusAttribute());
ASSERT_FALSE(GetAXObjectByElementId("a-hidden")->CanSetFocusAttribute());
// ...nor if inert...
ASSERT_FALSE(GetAXObjectByElementId("div-inert")->CanSetFocusAttribute());
ASSERT_FALSE(GetAXObjectByElementId("span-inert")->CanSetFocusAttribute());
ASSERT_FALSE(GetAXObjectByElementId("a-inert")->CanSetFocusAttribute());
// ...nor a combination of both.
ASSERT_FALSE(
GetAXObjectByElementId("div-hidden-inert")->CanSetFocusAttribute());
ASSERT_FALSE(
GetAXObjectByElementId("span-hidden-inert")->CanSetFocusAttribute());
ASSERT_FALSE(
GetAXObjectByElementId("a-hidden-inert")->CanSetFocusAttribute());
}
TEST_F(AccessibilityTest, GetParentNodeForComputeParent) {
SetBodyInnerHTML(
R"HTML(<img usemap="#map"><map name="map"><area id="area"
shape="rect" coords="0,0,5,5" href="about:blank" alt="Area">)HTML");
AXObjectCacheImpl& cache = GetAXObjectCache();
// The parent of the area isn't the DOM parent, but the image because that
// mirrors the structure of the ax tree.
Element* area = GetElementById("area");
AXObject* parent = AXObject::ComputeNonARIAParent(cache, area);
EXPECT_TRUE(IsA<HTMLImageElement>(parent->GetNode()));
parent = AXObject::ComputeNonARIAParent(cache, parent->GetNode());
EXPECT_TRUE(IsA<HTMLBodyElement>(parent->GetNode()));
parent = AXObject::ComputeNonARIAParent(cache, parent->GetNode());
EXPECT_TRUE(IsA<HTMLHtmlElement>(parent->GetNode()));
parent = AXObject::ComputeNonARIAParent(cache, parent->GetNode());
EXPECT_TRUE(IsA<Document>(parent->GetNode()));
parent = AXObject::ComputeNonARIAParent(cache, parent->GetNode());
EXPECT_EQ(parent, nullptr);
}
TEST_F(AccessibilityTest, CanComputeAsNaturalParent) {
SetBodyInnerHTML(R"HTML(M<img usemap="#map"><map name="map"><hr><progress>
<div><input type="range">M)HTML");
Element* elem = GetDocument().QuerySelector(AtomicString("img"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("map"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("hr"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("progress"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("input"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("div"));
EXPECT_TRUE(AXObject::CanComputeAsNaturalParent(elem));
elem = GetDocument().QuerySelector(AtomicString("input"));
EXPECT_FALSE(AXObject::CanComputeAsNaturalParent(elem));
}
TEST_F(AccessibilityTest, StitchChildTree) {
// Nodes that are descendants of the node at which a child tree was stitched
// (the host node) make all descendants accessibility ignored, hence the
// "ignored text" and "ignoredButton" nomenclature. The child tree will take
// their place.
//
// If the host node is accessibility ignored, it should be altered to become
// unignored, unless the host node was "ignored but included in tree" whereby
// a change is not necessary.
SetBodyInnerHTML(R"HTML(
<!-- role="banner" so that it is included in the tree. -->
<div id="div">
<p id="paragraph">Ignored text.</P>
</div>
<input id="button" type="button" value="Test"
style="display: none;" lang="fr-CA"> <!-- lang includes in tree -->
<canvas id="canvas" aria-hidden="true" lang="fr-CA">
<input id="ignoredButton" type="button" aria-hidden="false" value="Test">
<p aria-hidden="false>More fallback content.</p>
</canvas>)HTML");
AXObject* root = GetAXRootObject();
ASSERT_NE(nullptr, root);
root->LoadInlineTextBoxes();
AXObject* div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, div);
AXObject* paragraph = GetAXObjectByElementId("paragraph");
ASSERT_NE(nullptr, paragraph);
AXObject* paragraph_text = paragraph->DeepestFirstChildIncludingIgnored();
ASSERT_NE(nullptr, paragraph_text);
ASSERT_EQ(paragraph_text->RoleValue(),
ax::mojom::blink::Role::kInlineTextBox);
AXObject* button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
AXObject* canvas = GetAXObjectByElementId("canvas");
ASSERT_NE(nullptr, canvas);
AXObject* ignored_button = GetAXObjectByElementId("ignoredButton");
ASSERT_NE(nullptr, ignored_button);
EXPECT_TRUE(div->AccessibilityIsIncludedInTree());
EXPECT_TRUE(div->IsVisible());
EXPECT_EQ(1, div->ChildCountIncludingIgnored());
EXPECT_TRUE(paragraph->AccessibilityIsIncludedInTree());
EXPECT_TRUE(paragraph->IsVisible());
EXPECT_TRUE(paragraph_text->AccessibilityIsIncludedInTree());
EXPECT_TRUE(paragraph_text->IsVisible());
EXPECT_TRUE(button->AccessibilityIsIgnored());
EXPECT_FALSE(button->IsVisible());
EXPECT_TRUE(canvas->AccessibilityIsIgnored());
EXPECT_FALSE(canvas->IsVisible());
EXPECT_EQ(1, canvas->ChildCountIncludingIgnored());
EXPECT_TRUE(ignored_button->AccessibilityIsIncludedInTree());
EXPECT_FALSE(ignored_button->IsVisible());
EXPECT_FALSE(paragraph->IsHiddenByChildTree());
EXPECT_FALSE(paragraph_text->IsHiddenByChildTree());
EXPECT_FALSE(ignored_button->IsHiddenByChildTree());
ui::AXActionData action_data;
action_data.action = ax::mojom::blink::Action::kStitchChildTree;
const ui::AXTreeID div_child_tree_id = ui::AXTreeID::CreateNewAXTreeID();
action_data.target_node_id = div->AXObjectID();
action_data.child_tree_id = div_child_tree_id;
div->PerformAction(action_data);
const ui::AXTreeID button_child_tree_id = ui::AXTreeID::CreateNewAXTreeID();
action_data.target_node_id = button->AXObjectID();
action_data.child_tree_id = button_child_tree_id;
button->PerformAction(action_data);
const ui::AXTreeID canvas_child_tree_id = ui::AXTreeID::CreateNewAXTreeID();
action_data.target_node_id = canvas->AXObjectID();
action_data.child_tree_id = canvas_child_tree_id;
canvas->PerformAction(action_data);
ScopedFreezeAXCache freeze(GetAXObjectCache());
ui::AXNodeData div_node_data;
div->Serialize(&div_node_data, ui::AXMode::kScreenReader);
ui::AXNodeData button_node_data;
button->Serialize(&button_node_data, ui::AXMode::kScreenReader);
ui::AXNodeData canvas_node_data;
canvas->Serialize(&canvas_node_data, ui::AXMode::kScreenReader);
EXPECT_EQ(div_child_tree_id.ToString(),
div_node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kChildTreeId));
EXPECT_EQ(button_child_tree_id.ToString(),
button_node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kChildTreeId));
EXPECT_EQ(canvas_child_tree_id.ToString(),
canvas_node_data.GetStringAttribute(
ax::mojom::blink::StringAttribute::kChildTreeId));
// Fetch the hosting nodes again to ensure that we have their latest
// incarnations, if any.
div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, div);
button = GetAXObjectByElementId("button");
ASSERT_NE(nullptr, button);
canvas = GetAXObjectByElementId("canvas");
ASSERT_NE(nullptr, canvas);
EXPECT_TRUE(div->AccessibilityIsIncludedInTree());
EXPECT_TRUE(div->IsVisible());
EXPECT_EQ(0, div->ChildCountIncludingIgnored());
EXPECT_TRUE(button->AccessibilityIsIncludedInTree())
<< "`button` should switch from ignored due to `display:none`, to "
"included in the tree.";
EXPECT_FALSE(button->IsVisible())
<< "The visibility state should not change, only the inclusion in the "
"tree.";
EXPECT_EQ(0, button->ChildCountIncludingIgnored());
EXPECT_TRUE(canvas->AccessibilityIsIgnoredButIncludedInTree());
EXPECT_FALSE(canvas->IsVisible())
<< "The visibility state should not change, only the inclusion in the "
"tree.";
EXPECT_EQ(0, canvas->ChildCountIncludingIgnored());
// Re-create the detached objects and check that they are still "hidden by the
// child tree". We need to do this because Blink will re-create the objects
// once it walks the DOM tree again.
EXPECT_TRUE(paragraph->IsHiddenByChildTree());
EXPECT_TRUE(paragraph->AccessibilityIsIgnored());
EXPECT_FALSE(paragraph->IsVisible());
EXPECT_TRUE(ignored_button->IsHiddenByChildTree());
EXPECT_TRUE(ignored_button->AccessibilityIsIgnored());
EXPECT_FALSE(ignored_button->IsVisible());
}
TEST_F(AccessibilityTest, UpdateTreeUpdatesInheritedLiveProperty) {
SetBodyInnerHTML(R"HTML(
<main id="main">
<p>some text</p>
<div>
<blockquote>
<mark id="mark">
nested text
</mark>
</blockquote>
</div>
</main>
)HTML");
AXObject* main = GetAXObjectByElementId("main");
ASSERT_NE(nullptr, main);
main->GetElement()->setAttribute(html_names::kAriaLiveAttr, "polite",
ASSERT_NO_EXCEPTION);
GetAXObjectCache().UpdateAXForAllDocuments();
AXObject* mark = GetAXObjectByElementId("mark");
ASSERT_NE(nullptr, mark);
// Ensure the new live region status has propagated to a deep descendant.
ASSERT_NE(nullptr, mark->ContainerLiveRegionStatus());
}
TEST_F(AccessibilityTest, UpdateTreeUpdatesInheritedAriaHiddenProperty) {
SetBodyInnerHTML(R"HTML(
<main id="main">
<p>some text</p>
<div>
<blockquote>
<mark id="mark">
nested text
</mark>
</blockquote>
</div>
</main>
)HTML");
AXObject* main = GetAXObjectByElementId("main");
ASSERT_NE(nullptr, main);
main->GetElement()->setAttribute(html_names::kAriaHiddenAttr, "true",
ASSERT_NO_EXCEPTION);
GetAXObjectCache().UpdateAXForAllDocuments();
AXObject* mark = GetAXObjectByElementId("mark");
ASSERT_NE(nullptr, mark);
// Ensure that aria-hidden has propagated to a deep descendant.
ASSERT_TRUE(mark->IsAriaHidden());
main = GetAXObjectByElementId("main");
main->GetElement()->removeAttribute(html_names::kAriaHiddenAttr);
GetAXObjectCache().UpdateAXForAllDocuments();
// Ensure that clearing aria-hidden has propagated to a deep descendant.
mark = GetAXObjectByElementId("mark");
ASSERT_FALSE(mark->IsAriaHidden());
}
TEST_F(AccessibilityTest, UpdateTreeUpdatesInheritedInertProperty) {
SetBodyInnerHTML(R"HTML(
<main id="main">
<p>some text</p>
<div>
<blockquote>
<mark id="mark">
nested text
</mark>
</blockquote>
</div>
</main>
)HTML");
AXObject* main = GetAXObjectByElementId("main");
ASSERT_NE(nullptr, main);
main->GetElement()->setAttribute(html_names::kInertAttr, "true",
ASSERT_NO_EXCEPTION);
GetAXObjectCache().UpdateAXForAllDocuments();
AXObject* mark = GetAXObjectByElementId("mark");
ASSERT_NE(nullptr, mark);
// Ensure inertness has propagated to a deep descendant.
ASSERT_TRUE(mark->IsInert());
}
TEST_F(AccessibilityTest, UpdateTreeUpdatesInheritedDisabledProperty) {
SetBodyInnerHTML(R"HTML(
<fieldset id="fieldset">
<p>some text</p>
<div>
<blockquote>
<mark id="mark">
nested text
</mark>
</blockquote>
</div>
</fieldset>
)HTML");
AXObject* fieldset = GetAXObjectByElementId("fieldset");
ASSERT_NE(nullptr, fieldset);
fieldset->GetElement()->setAttribute(html_names::kAriaDisabledAttr, "true",
ASSERT_NO_EXCEPTION);
GetAXObjectCache().UpdateAXForAllDocuments();
AXObject* mark = GetAXObjectByElementId("mark");
ASSERT_NE(nullptr, mark);
// Ensure that "ancestor is disabled" has propagated to a deep descendant.
ASSERT_TRUE(mark->IsDescendantOfDisabledNode());
}
} // namespace test
} // namespace blink