blob: 7d26ab8301a39a32874a0c47946860fefdd93aab [file] [log] [blame]
// Copyright 2016 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 <memory>
#import <Cocoa/Cocoa.h>
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#import "testing/gtest_mac.h"
#include "ui/accessibility/ax_enums.h"
#include "ui/accessibility/ax_node_data.h"
#import "ui/accessibility/platform/ax_platform_node_mac.h"
#include "ui/base/ime/text_input_type.h"
#import "ui/gfx/mac/coordinate_conversion.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/widget.h"
// Expose some methods from AXPlatformNodeCocoa for testing purposes only.
@interface AXPlatformNodeCocoa (Testing)
- (NSString*)AXRole;
@end
namespace views {
namespace {
NSString* const kTestPlaceholderText = @"Test placeholder text";
NSString* const kTestStringValue = @"Test string value";
NSString* const kTestTitle = @"Test textfield";
class FlexibleRoleTestView : public View {
public:
explicit FlexibleRoleTestView(ui::AXRole role) : role_(role) {}
void set_role(ui::AXRole role) { role_ = role; }
// Add a child view and resize to fit the child.
void FitBoundsToNewChild(View* view) {
AddChildView(view);
// Fit the parent widget to the size of the child for accurate hit tests.
SetBoundsRect(view->bounds());
}
bool mouse_was_pressed() const { return mouse_was_pressed_; }
// View:
void GetAccessibleNodeData(ui::AXNodeData* node_data) override {
View::GetAccessibleNodeData(node_data);
node_data->role = role_;
}
bool OnMousePressed(const ui::MouseEvent& event) override {
mouse_was_pressed_ = true;
return false;
}
private:
ui::AXRole role_;
bool mouse_was_pressed_ = false;
DISALLOW_COPY_AND_ASSIGN(FlexibleRoleTestView);
};
class NativeWidgetMacAccessibilityTest : public test::WidgetTest {
public:
NativeWidgetMacAccessibilityTest() {}
void SetUp() override {
test::WidgetTest::SetUp();
widget_ = CreateTopLevelPlatformWidget();
widget_->SetBounds(gfx::Rect(50, 50, 100, 100));
widget()->Show();
}
void TearDown() override {
widget_->CloseNow();
test::WidgetTest::TearDown();
}
id A11yElementAtMidpoint() {
// Accessibility hit tests come in Cocoa screen coordinates.
NSPoint midpoint_in_screen_ = gfx::ScreenPointToNSPoint(
widget_->GetWindowBoundsInScreen().CenterPoint());
return
[widget_->GetNativeWindow() accessibilityHitTest:midpoint_in_screen_];
}
id AttributeValueAtMidpoint(NSString* attribute) {
return [A11yElementAtMidpoint() accessibilityAttributeValue:attribute];
}
Textfield* AddChildTextfield(const gfx::Size& size) {
Textfield* textfield = new Textfield;
textfield->SetText(base::SysNSStringToUTF16(kTestStringValue));
textfield->SetAccessibleName(base::SysNSStringToUTF16(kTestTitle));
textfield->SetSize(size);
widget()->GetContentsView()->AddChildView(textfield);
return textfield;
}
Widget* widget() { return widget_; }
gfx::Rect GetWidgetBounds() { return widget_->GetClientAreaBoundsInScreen(); }
private:
Widget* widget_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(NativeWidgetMacAccessibilityTest);
};
} // namespace
// Test for NSAccessibilityChildrenAttribute, and ensure it excludes ignored
// children from the accessibility tree.
TEST_F(NativeWidgetMacAccessibilityTest, ChildrenAttribute) {
// Check childless views don't have accessibility children.
EXPECT_EQ(0u,
[AttributeValueAtMidpoint(NSAccessibilityChildrenAttribute) count]);
const size_t kNumChildren = 3;
for (size_t i = 0; i < kNumChildren; ++i) {
// Make sure the labels won't interfere with the hit test.
AddChildTextfield(gfx::Size());
}
EXPECT_EQ(kNumChildren,
[AttributeValueAtMidpoint(NSAccessibilityChildrenAttribute) count]);
// Check ignored children don't show up in the accessibility tree.
widget()->GetContentsView()->AddChildView(
new FlexibleRoleTestView(ui::AX_ROLE_IGNORED));
EXPECT_EQ(kNumChildren,
[AttributeValueAtMidpoint(NSAccessibilityChildrenAttribute) count]);
}
// Test for NSAccessibilityParentAttribute, including for a Widget with no
// parent.
TEST_F(NativeWidgetMacAccessibilityTest, ParentAttribute) {
Textfield* child = AddChildTextfield(widget()->GetContentsView()->size());
// Views with Widget parents will have a NSWindow parent.
EXPECT_NSEQ(
NSAccessibilityWindowRole,
[AttributeValueAtMidpoint(NSAccessibilityParentAttribute) AXRole]);
// Views with non-Widget parents will have the role of the parent view.
widget()->GetContentsView()->RemoveChildView(child);
FlexibleRoleTestView* parent = new FlexibleRoleTestView(ui::AX_ROLE_GROUP);
parent->FitBoundsToNewChild(child);
widget()->GetContentsView()->AddChildView(parent);
EXPECT_NSEQ(
NSAccessibilityGroupRole,
[AttributeValueAtMidpoint(NSAccessibilityParentAttribute) AXRole]);
// Test an ignored role parent is skipped in favor of the grandparent.
parent->set_role(ui::AX_ROLE_IGNORED);
EXPECT_NSEQ(
NSAccessibilityWindowRole,
[AttributeValueAtMidpoint(NSAccessibilityParentAttribute) AXRole]);
}
// Test for NSAccessibilityPositionAttribute, including on Widget movement
// updates.
TEST_F(NativeWidgetMacAccessibilityTest, PositionAttribute) {
NSValue* widget_origin =
[NSValue valueWithPoint:gfx::ScreenPointToNSPoint(
GetWidgetBounds().bottom_left())];
EXPECT_NSEQ(widget_origin,
AttributeValueAtMidpoint(NSAccessibilityPositionAttribute));
// Check the attribute is updated when the Widget is moved.
gfx::Rect new_bounds(60, 80, 100, 100);
widget()->SetBounds(new_bounds);
widget_origin = [NSValue
valueWithPoint:gfx::ScreenPointToNSPoint(new_bounds.bottom_left())];
EXPECT_NSEQ(widget_origin,
AttributeValueAtMidpoint(NSAccessibilityPositionAttribute));
}
// Test for NSAccessibilityHelpAttribute.
TEST_F(NativeWidgetMacAccessibilityTest, HelpAttribute) {
Label* label = new Label(base::SysNSStringToUTF16(kTestPlaceholderText));
label->SetSize(GetWidgetBounds().size());
EXPECT_NSEQ(@"", AttributeValueAtMidpoint(NSAccessibilityHelpAttribute));
label->SetTooltipText(base::SysNSStringToUTF16(kTestPlaceholderText));
widget()->GetContentsView()->AddChildView(label);
EXPECT_NSEQ(kTestPlaceholderText,
AttributeValueAtMidpoint(NSAccessibilityHelpAttribute));
}
// Test for NSAccessibilityWindowAttribute and
// NSAccessibilityTopLevelUIElementAttribute.
TEST_F(NativeWidgetMacAccessibilityTest, WindowAndTopLevelUIElementAttributes) {
FlexibleRoleTestView* view = new FlexibleRoleTestView(ui::AX_ROLE_GROUP);
view->SetSize(GetWidgetBounds().size());
widget()->GetContentsView()->AddChildView(view);
// Make sure it's |view| in the hit test by checking its accessibility role.
EXPECT_EQ(NSAccessibilityGroupRole,
AttributeValueAtMidpoint(NSAccessibilityRoleAttribute));
EXPECT_NSEQ(widget()->GetNativeWindow(),
AttributeValueAtMidpoint(NSAccessibilityWindowAttribute));
EXPECT_NSEQ(
widget()->GetNativeWindow(),
AttributeValueAtMidpoint(NSAccessibilityTopLevelUIElementAttribute));
}
// Tests for accessibility attributes on a views::Textfield.
// TODO(patricialor): Test against Cocoa-provided attributes as well to ensure
// consistency between Cocoa and toolkit-views.
TEST_F(NativeWidgetMacAccessibilityTest, TextfieldGenericAttributes) {
Textfield* textfield = AddChildTextfield(GetWidgetBounds().size());
// NSAccessibilityEnabledAttribute.
textfield->SetEnabled(false);
EXPECT_EQ(NO, [AttributeValueAtMidpoint(NSAccessibilityEnabledAttribute)
boolValue]);
textfield->SetEnabled(true);
EXPECT_EQ(YES, [AttributeValueAtMidpoint(NSAccessibilityEnabledAttribute)
boolValue]);
// NSAccessibilityFocusedAttribute.
EXPECT_EQ(NO, [AttributeValueAtMidpoint(NSAccessibilityFocusedAttribute)
boolValue]);
textfield->RequestFocus();
EXPECT_EQ(YES, [AttributeValueAtMidpoint(NSAccessibilityFocusedAttribute)
boolValue]);
// NSAccessibilityTitleAttribute.
EXPECT_NSEQ(kTestTitle,
AttributeValueAtMidpoint(NSAccessibilityTitleAttribute));
// NSAccessibilityValueAttribute.
EXPECT_NSEQ(kTestStringValue,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
// NSAccessibilityRoleAttribute.
EXPECT_NSEQ(NSAccessibilityTextFieldRole,
AttributeValueAtMidpoint(NSAccessibilityRoleAttribute));
// NSAccessibilitySubroleAttribute and
// NSAccessibilityRoleDescriptionAttribute.
EXPECT_NSEQ(nil, AttributeValueAtMidpoint(NSAccessibilitySubroleAttribute));
NSString* role_description =
NSAccessibilityRoleDescription(NSAccessibilityTextFieldRole, nil);
EXPECT_NSEQ(role_description, AttributeValueAtMidpoint(
NSAccessibilityRoleDescriptionAttribute));
// Test accessibility clients can see subroles as well.
textfield->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
EXPECT_NSEQ(NSAccessibilitySecureTextFieldSubrole,
AttributeValueAtMidpoint(NSAccessibilitySubroleAttribute));
role_description = NSAccessibilityRoleDescription(
NSAccessibilityTextFieldRole, NSAccessibilitySecureTextFieldSubrole);
EXPECT_NSEQ(role_description, AttributeValueAtMidpoint(
NSAccessibilityRoleDescriptionAttribute));
// Prevent the textfield from interfering with hit tests on the widget itself.
widget()->GetContentsView()->RemoveChildView(textfield);
// NSAccessibilitySizeAttribute.
EXPECT_EQ(GetWidgetBounds().size(),
gfx::Size([AttributeValueAtMidpoint(NSAccessibilitySizeAttribute)
sizeValue]));
// Check the attribute is updated when the Widget is resized.
gfx::Size new_size(200, 40);
widget()->SetSize(new_size);
EXPECT_EQ(new_size, gfx::Size([AttributeValueAtMidpoint(
NSAccessibilitySizeAttribute) sizeValue]));
}
TEST_F(NativeWidgetMacAccessibilityTest, TextfieldEditableAttributes) {
Textfield* textfield = AddChildTextfield(GetWidgetBounds().size());
textfield->set_placeholder_text(
base::SysNSStringToUTF16(kTestPlaceholderText));
// NSAccessibilityInsertionPointLineNumberAttribute.
EXPECT_EQ(0, [AttributeValueAtMidpoint(
NSAccessibilityInsertionPointLineNumberAttribute) intValue]);
// NSAccessibilityNumberOfCharactersAttribute.
EXPECT_EQ(
kTestStringValue.length,
[AttributeValueAtMidpoint(NSAccessibilityNumberOfCharactersAttribute)
unsignedIntegerValue]);
// NSAccessibilityPlaceholderAttribute.
EXPECT_NSEQ(
kTestPlaceholderText,
AttributeValueAtMidpoint(NSAccessibilityPlaceholderValueAttribute));
// NSAccessibilitySelectedTextAttribute and
// NSAccessibilitySelectedTextRangeAttribute.
EXPECT_NSEQ(@"",
AttributeValueAtMidpoint(NSAccessibilitySelectedTextAttribute));
// The cursor will be at the end of the textfield, so the selection range will
// span 0 characters and be located at the index after the last character.
EXPECT_EQ(gfx::Range(kTestStringValue.length, kTestStringValue.length),
gfx::Range([AttributeValueAtMidpoint(
NSAccessibilitySelectedTextRangeAttribute) rangeValue]));
// Select some text in the middle of the textfield.
gfx::Range selection_range(2, 6);
textfield->SelectRange(selection_range);
EXPECT_NSEQ([kTestStringValue substringWithRange:selection_range.ToNSRange()],
AttributeValueAtMidpoint(NSAccessibilitySelectedTextAttribute));
EXPECT_EQ(selection_range,
gfx::Range([AttributeValueAtMidpoint(
NSAccessibilitySelectedTextRangeAttribute) rangeValue]));
// NSAccessibilityVisibleCharacterRangeAttribute.
EXPECT_EQ(gfx::Range(0, kTestStringValue.length),
gfx::Range([AttributeValueAtMidpoint(
NSAccessibilityVisibleCharacterRangeAttribute) rangeValue]));
}
// Test writing accessibility attributes via an accessibility client for normal
// Views.
TEST_F(NativeWidgetMacAccessibilityTest, ViewWritableAttributes) {
FlexibleRoleTestView* view = new FlexibleRoleTestView(ui::AX_ROLE_GROUP);
view->SetSize(GetWidgetBounds().size());
widget()->GetContentsView()->AddChildView(view);
// Make sure the accessibility object tested is the correct one.
id ax_node = A11yElementAtMidpoint();
EXPECT_TRUE(ax_node);
EXPECT_NSEQ(NSAccessibilityGroupRole,
AttributeValueAtMidpoint(NSAccessibilityRoleAttribute));
// Make sure |view| is focusable, then focus/unfocus it.
view->SetFocusBehavior(View::FocusBehavior::ALWAYS);
EXPECT_FALSE(view->HasFocus());
EXPECT_FALSE(
[AttributeValueAtMidpoint(NSAccessibilityFocusedAttribute) boolValue]);
EXPECT_TRUE([ax_node
accessibilityIsAttributeSettable:NSAccessibilityFocusedAttribute]);
[ax_node accessibilitySetValue:[NSNumber numberWithBool:YES]
forAttribute:NSAccessibilityFocusedAttribute];
EXPECT_TRUE(
[AttributeValueAtMidpoint(NSAccessibilityFocusedAttribute) boolValue]);
EXPECT_TRUE(view->HasFocus());
}
// Test writing accessibility attributes via an accessibility client for
// editable controls (in this case, views::Textfields).
TEST_F(NativeWidgetMacAccessibilityTest, TextfieldWritableAttributes) {
Textfield* textfield = AddChildTextfield(GetWidgetBounds().size());
// Get the Textfield accessibility object.
NSPoint midpoint = gfx::ScreenPointToNSPoint(GetWidgetBounds().CenterPoint());
id ax_node = [widget()->GetNativeWindow() accessibilityHitTest:midpoint];
EXPECT_TRUE(ax_node);
// Make sure it's the correct accessibility object.
id value =
[ax_node accessibilityAttributeValue:NSAccessibilityValueAttribute];
EXPECT_NSEQ(kTestStringValue, value);
// Write a new NSAccessibilityValueAttribute.
EXPECT_TRUE(
[ax_node accessibilityIsAttributeSettable:NSAccessibilityValueAttribute]);
[ax_node accessibilitySetValue:kTestPlaceholderText
forAttribute:NSAccessibilityValueAttribute];
EXPECT_NSEQ(kTestPlaceholderText,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
EXPECT_EQ(base::SysNSStringToUTF16(kTestPlaceholderText), textfield->text());
// Test a read-only textfield.
textfield->SetReadOnly(true);
EXPECT_FALSE(
[ax_node accessibilityIsAttributeSettable:NSAccessibilityValueAttribute]);
[ax_node accessibilitySetValue:kTestStringValue
forAttribute:NSAccessibilityValueAttribute];
EXPECT_NSEQ(kTestPlaceholderText,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
EXPECT_EQ(base::SysNSStringToUTF16(kTestPlaceholderText), textfield->text());
textfield->SetReadOnly(false);
// Change the selection text when there is no selected text.
textfield->SelectRange(gfx::Range(0, 0));
EXPECT_TRUE([ax_node
accessibilityIsAttributeSettable:NSAccessibilitySelectedTextAttribute]);
NSString* new_string =
[kTestStringValue stringByAppendingString:kTestPlaceholderText];
[ax_node accessibilitySetValue:kTestStringValue
forAttribute:NSAccessibilitySelectedTextAttribute];
EXPECT_NSEQ(new_string,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
EXPECT_EQ(base::SysNSStringToUTF16(new_string), textfield->text());
// Replace entire selection.
gfx::Range test_range(0, [new_string length]);
textfield->SelectRange(test_range);
[ax_node accessibilitySetValue:kTestStringValue
forAttribute:NSAccessibilitySelectedTextAttribute];
EXPECT_NSEQ(kTestStringValue,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
EXPECT_EQ(base::SysNSStringToUTF16(kTestStringValue), textfield->text());
// Make sure the cursor is at the end of the Textfield.
EXPECT_EQ(gfx::Range([kTestStringValue length]),
textfield->GetSelectedRange());
// Replace a middle section only (with a backwards selection range).
base::string16 front = base::ASCIIToUTF16("Front ");
base::string16 middle = base::ASCIIToUTF16("middle");
base::string16 back = base::ASCIIToUTF16(" back");
base::string16 replacement = base::ASCIIToUTF16("replaced");
textfield->SetText(front + middle + back);
test_range = gfx::Range(front.length() + middle.length(), front.length());
new_string = base::SysUTF16ToNSString(front + replacement + back);
textfield->SelectRange(test_range);
[ax_node accessibilitySetValue:base::SysUTF16ToNSString(replacement)
forAttribute:NSAccessibilitySelectedTextAttribute];
EXPECT_NSEQ(new_string,
AttributeValueAtMidpoint(NSAccessibilityValueAttribute));
EXPECT_EQ(base::SysNSStringToUTF16(new_string), textfield->text());
// Make sure the cursor is at the end of the replacement.
EXPECT_EQ(gfx::Range(front.length() + replacement.length()),
textfield->GetSelectedRange());
// Check it's not possible to change the selection range when read-only. Note
// that this behavior is inconsistent with Cocoa - selections can be set via
// a11y in selectable NSTextfields (unless they are password fields).
// https://crbug.com/692362
textfield->SetReadOnly(true);
EXPECT_FALSE([ax_node accessibilityIsAttributeSettable:
NSAccessibilitySelectedTextRangeAttribute]);
textfield->SetReadOnly(false);
EXPECT_TRUE([ax_node accessibilityIsAttributeSettable:
NSAccessibilitySelectedTextRangeAttribute]);
// Change the selection to a valid range within the text.
[ax_node accessibilitySetValue:[NSValue valueWithRange:NSMakeRange(2, 5)]
forAttribute:NSAccessibilitySelectedTextRangeAttribute];
EXPECT_EQ(gfx::Range(2, 7), textfield->GetSelectedRange());
// If the length is longer than the value length, default to the max possible.
[ax_node accessibilitySetValue:[NSValue valueWithRange:NSMakeRange(0, 1000)]
forAttribute:NSAccessibilitySelectedTextRangeAttribute];
EXPECT_EQ(gfx::Range(0, textfield->text().length()),
textfield->GetSelectedRange());
// Check just moving the cursor works, too.
[ax_node accessibilitySetValue:[NSValue valueWithRange:NSMakeRange(5, 0)]
forAttribute:NSAccessibilitySelectedTextRangeAttribute];
EXPECT_EQ(gfx::Range(5, 5), textfield->GetSelectedRange());
}
// Test performing a 'click' on Views with clickable roles work.
TEST_F(NativeWidgetMacAccessibilityTest, PressAction) {
FlexibleRoleTestView* view = new FlexibleRoleTestView(ui::AX_ROLE_BUTTON);
widget()->GetContentsView()->AddChildView(view);
view->SetSize(GetWidgetBounds().size());
id ax_node = A11yElementAtMidpoint();
EXPECT_NSEQ(NSAccessibilityButtonRole,
AttributeValueAtMidpoint(NSAccessibilityRoleAttribute));
EXPECT_TRUE([[ax_node accessibilityActionNames]
containsObject:NSAccessibilityPressAction]);
[ax_node accessibilityPerformAction:NSAccessibilityPressAction];
EXPECT_TRUE(view->mouse_was_pressed());
}
} // namespace views