blob: 6bc4c93c2788f1a190ddf50e1b26b8bbcac419ba [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 "chrome/test/views/accessibility_checker.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/widget/native_widget_delegate.h"
#include "ui/views/widget/widget.h"
namespace {
using ax::mojom::NameFrom;
using ax::mojom::Role;
using ax::mojom::State;
using ax::mojom::StringAttribute;
// Return helpful string for identifying a view.
// Includes the view class of every view in the ancestor chain, root first.
// Also provides the id.
// For example:
// BrowserView > OmniboxView (id 3).
std::string GetViewDebugString(const views::View* view) {
// Get classes of ancestors.
std::vector<std::string> classes;
for (const views::View* ancestor = view; ancestor;
ancestor = ancestor->parent())
classes.insert(classes.begin(), ancestor->GetClassName());
return base::JoinString(classes, " > ") +
base::StringPrintf(" (id %d)", view->id());
}
bool DoesViewHaveAccessibleNameOrLabelError(ui::AXNodeData* data) {
// Focusable nodes must have an accessible name, otherwise screen reader users
// will not know what they landed on. For example, the reload button should
// have an accessible name of "Reload".
// Exceptions:
// 1) Textfields can set the placeholder string attribute.
// 2) Explicitly setting the name to "" is allowed if the view uses
// AXNodedata.SetNameExplicitlyEmpty().
// It has a name, we're done.
if (!data->GetStringAttribute(StringAttribute::kName).empty())
return false;
// Text fields are allowed to have a placeholder instead.
if (data->role == Role::kTextField &&
!data->GetStringAttribute(StringAttribute::kPlaceholder).empty())
return false;
// Finally, a view is allowed to explicitly state that it has no name.
if (data->GetNameFrom() == NameFrom::kAttributeExplicitlyEmpty)
return false;
// Has an error -- no name or placeholder, and not explicitly empty.
return true;
}
bool DoesViewHaveAccessibilityErrors(views::View* view,
std::string* error_message) {
views::ViewAccessibility& view_accessibility = view->GetViewAccessibility();
ui::AXNodeData node_data;
// Get accessible node data from view_accessibility instead of view, because
// some additional fields are processed and set there.
view_accessibility.GetAccessibleNodeData(&node_data);
std::string violations;
// No checks for unfocusable items yet.
if (node_data.HasState(State::kFocusable)) {
if (DoesViewHaveAccessibleNameOrLabelError(&node_data)) {
violations +=
"\n- Focusable View has no accessible name or placeholder, and the "
"name attribute does not use kAttributeExplicitlyEmpty.";
}
if (node_data.HasState(State::kInvisible))
violations += "\n- Focusable View should not be invisible.";
}
if (violations.empty())
return false; // No errors.
*error_message =
"The following view violates DoesViewHaveAccessibilityErrors() when its "
"widget becomes " +
std::string(view->GetWidget()->IsVisible() ? "visible:\n" : "hidden:\n") +
GetViewDebugString(view) + violations +
"\n\nNote: for a more useful error message that includes a stack of how "
"this view was constructed, use git cl patch 963284. Please leave a note "
"on that CL if you find it useful.";
return true;
}
bool DoesViewHaveAccessibilityErrorsRecursive(views::View* view,
std::string* error_message) {
if (DoesViewHaveAccessibilityErrors(view, error_message))
return true;
for (int i = 0; i < view->child_count(); ++i) {
if (DoesViewHaveAccessibilityErrorsRecursive(view->child_at(i),
error_message))
return true;
}
return false; // All views in this subtree passed all checker.
}
} // namespace
void AddFailureOnWidgetAccessibilityError(views::Widget* widget) {
std::string error_message;
if (widget->widget_delegate() && !widget->IsClosed() &&
widget->GetRootView() &&
DoesViewHaveAccessibilityErrorsRecursive(widget->GetRootView(),
&error_message)) {
ADD_FAILURE() << error_message;
}
}
AccessibilityChecker::AccessibilityChecker() : scoped_observer_(this) {}
AccessibilityChecker::~AccessibilityChecker() {
DCHECK(!scoped_observer_.IsObservingSources());
}
void AccessibilityChecker::OnBeforeWidgetInit(
views::Widget::InitParams* params,
views::internal::NativeWidgetDelegate* delegate) {
ChromeViewsDelegate::OnBeforeWidgetInit(params, delegate);
views::Widget* widget = delegate->AsWidget();
if (widget)
scoped_observer_.Add(widget);
}
void AccessibilityChecker::OnWidgetDestroying(views::Widget* widget) {
scoped_observer_.Remove(widget);
}
void AccessibilityChecker::OnWidgetVisibilityChanged(views::Widget* widget,
bool visible) {
// Test widget for accessibility errors both as it becomes visible or hidden,
// in order to catch more errors. For example, to catch errors in the download
// shelf we must check the browser window as it is hidden, because the shelf
// is not visible when the browser window first appears.
AddFailureOnWidgetAccessibilityError(widget);
}