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