1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/test/views/accessibility_checker.h"
6
7 #include "base/strings/string_util.h"
8 #include "base/strings/stringprintf.h"
9 #include "testing/gtest/include/gtest/gtest.h"
10 #include "ui/accessibility/ax_node_data.h"
11 #include "ui/views/accessibility/view_accessibility.h"
12 #include "ui/views/widget/native_widget_delegate.h"
13 #include "ui/views/widget/widget.h"
14
15 namespace {
16
17 using ax::mojom::NameFrom;
18 using ax::mojom::Role;
19 using ax::mojom::State;
20 using ax::mojom::StringAttribute;
21
22 // Return helpful string for identifying a view.
23 // Includes the view class of every view in the ancestor chain, root first.
24 // Also provides the id.
25 // For example:
26 // BrowserView > OmniboxView (id 3).
GetViewDebugString(const views::View * view)27 std::string GetViewDebugString(const views::View* view) {
28 // Get classes of ancestors.
29 std::vector<std::string> classes;
30 for (const views::View* ancestor = view; ancestor;
31 ancestor = ancestor->parent())
32 classes.insert(classes.begin(), ancestor->GetClassName());
33
34 return base::JoinString(classes, " > ") +
35 base::StringPrintf(" (id %d)", view->GetID());
36 }
37
DoesViewHaveAccessibleNameOrLabelError(ui::AXNodeData * data)38 bool DoesViewHaveAccessibleNameOrLabelError(ui::AXNodeData* data) {
39 // Focusable nodes must have an accessible name, otherwise screen reader users
40 // will not know what they landed on. For example, the reload button should
41 // have an accessible name of "Reload".
42 // Exceptions:
43 // 1) Textfields can set the placeholder string attribute.
44 // 2) Explicitly setting the name to "" is allowed if the view uses
45 // AXNodedata.SetNameExplicitlyEmpty().
46
47 // It has a name, we're done.
48 if (!data->GetStringAttribute(StringAttribute::kName).empty())
49 return false;
50
51 // Text fields are allowed to have a placeholder instead.
52 if (data->role == Role::kTextField &&
53 !data->GetStringAttribute(StringAttribute::kPlaceholder).empty())
54 return false;
55
56 // Finally, a view is allowed to explicitly state that it has no name.
57 if (data->GetNameFrom() == NameFrom::kAttributeExplicitlyEmpty)
58 return false;
59
60 // Has an error -- no name or placeholder, and not explicitly empty.
61 return true;
62 }
63
DoesViewHaveAccessibilityErrors(views::View * view,std::string * error_message)64 bool DoesViewHaveAccessibilityErrors(views::View* view,
65 std::string* error_message) {
66 views::ViewAccessibility& view_accessibility = view->GetViewAccessibility();
67 ui::AXNodeData node_data;
68 // Get accessible node data from view_accessibility instead of view, because
69 // some additional fields are processed and set there.
70 view_accessibility.GetAccessibleNodeData(&node_data);
71
72 std::string violations;
73
74 // No checks for unfocusable items yet.
75 if (node_data.HasState(State::kFocusable)) {
76 if (DoesViewHaveAccessibleNameOrLabelError(&node_data)) {
77 violations +=
78 "\n- Focusable View has no accessible name or placeholder, and the "
79 "name attribute does not use kAttributeExplicitlyEmpty.";
80 }
81 if (node_data.HasState(State::kInvisible))
82 violations += "\n- Focusable View should not be invisible.";
83 }
84
85 if (violations.empty())
86 return false; // No errors.
87
88 *error_message =
89 "The following view violates DoesViewHaveAccessibilityErrors() when its "
90 "widget becomes " +
91 std::string(view->GetWidget()->IsVisible() ? "visible:\n" : "hidden:\n") +
92 GetViewDebugString(view) + violations +
93 "\n\nNote: for a more useful error message that includes a stack of how "
94 "this view was constructed, use git cl patch 963284. Please leave a note "
95 "on that CL if you find it useful.";
96 return true;
97 }
98
DoesViewHaveAccessibilityErrorsRecursive(views::View * view,std::string * error_message)99 bool DoesViewHaveAccessibilityErrorsRecursive(views::View* view,
100 std::string* error_message) {
101 const auto recurse = [error_message](auto* v) {
102 return DoesViewHaveAccessibilityErrorsRecursive(v, error_message);
103 };
104 return DoesViewHaveAccessibilityErrors(view, error_message) ||
105 std::any_of(view->children().begin(), view->children().end(), recurse);
106 }
107
108 } // namespace
109
AddFailureOnWidgetAccessibilityError(views::Widget * widget)110 void AddFailureOnWidgetAccessibilityError(views::Widget* widget) {
111 std::string error_message;
112 if (widget->widget_delegate() && !widget->IsClosed() &&
113 widget->GetRootView() &&
114 DoesViewHaveAccessibilityErrorsRecursive(widget->GetRootView(),
115 &error_message)) {
116 ADD_FAILURE() << error_message;
117 }
118 }
119
AccessibilityChecker()120 AccessibilityChecker::AccessibilityChecker() : scoped_observer_(this) {}
121
~AccessibilityChecker()122 AccessibilityChecker::~AccessibilityChecker() {
123 DCHECK(!scoped_observer_.IsObservingSources());
124 }
125
OnBeforeWidgetInit(views::Widget::InitParams * params,views::internal::NativeWidgetDelegate * delegate)126 void AccessibilityChecker::OnBeforeWidgetInit(
127 views::Widget::InitParams* params,
128 views::internal::NativeWidgetDelegate* delegate) {
129 ChromeViewsDelegate::OnBeforeWidgetInit(params, delegate);
130 views::Widget* widget = delegate->AsWidget();
131 if (widget)
132 scoped_observer_.Add(widget);
133 }
134
OnWidgetDestroying(views::Widget * widget)135 void AccessibilityChecker::OnWidgetDestroying(views::Widget* widget) {
136 scoped_observer_.Remove(widget);
137 }
138
OnWidgetVisibilityChanged(views::Widget * widget,bool visible)139 void AccessibilityChecker::OnWidgetVisibilityChanged(views::Widget* widget,
140 bool visible) {
141 // Test widget for accessibility errors both as it becomes visible or hidden,
142 // in order to catch more errors. For example, to catch errors in the download
143 // shelf we must check the browser window as it is hidden, because the shelf
144 // is not visible when the browser window first appears.
145 AddFailureOnWidgetAccessibilityError(widget);
146 }
147