blob: f5d347bf6e57356da33331c3f4e29dbcbe175451 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/public/test/browser_test.h"
#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h"
#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h"
#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "content/browser/accessibility/browser_accessibility.h"
#include "content/browser/accessibility/browser_accessibility_com_win.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "ui/accessibility/accessibility_features.h"
using Microsoft::WRL::ComPtr;
namespace content {
#define ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(safearray, expected_size) \
{ \
EXPECT_EQ(sizeof(ITextRangeProvider*), ::SafeArrayGetElemsize(safearray)); \
ASSERT_EQ(1u, SafeArrayGetDim(safearray)); \
LONG array_lower_bound; \
ASSERT_HRESULT_SUCCEEDED( \
SafeArrayGetLBound(safearray, 1, &array_lower_bound)); \
LONG array_upper_bound; \
ASSERT_HRESULT_SUCCEEDED( \
SafeArrayGetUBound(safearray, 1, &array_upper_bound)); \
size_t count = array_upper_bound - array_lower_bound + 1; \
ASSERT_EQ(expected_size, count); \
}
#define EXPECT_UIA_TEXTRANGE_EQ(provider, expected_content) \
{ \
base::win::ScopedBstr provider_content; \
ASSERT_HRESULT_SUCCEEDED( \
provider->GetText(-1, provider_content.Receive())); \
EXPECT_STREQ(expected_content, provider_content.Get()); \
}
class AXPlatformNodeTextProviderWinBrowserTest : public ContentBrowserTest {
protected:
void LoadInitialAccessibilityTreeFromUrl(
const GURL& url,
ui::AXMode accessibility_mode = ui::kAXModeComplete) {
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
accessibility_mode,
ax::mojom::Event::kLoadComplete);
EXPECT_TRUE(NavigateToURL(shell(), url));
ASSERT_TRUE(waiter.WaitForNotification());
}
void LoadInitialAccessibilityTreeFromHtmlFilePath(
const std::string& html_file_path,
ui::AXMode accessibility_mode = ui::kAXModeComplete) {
if (!embedded_test_server()->Started())
ASSERT_TRUE(embedded_test_server()->Start());
ASSERT_TRUE(embedded_test_server()->Started());
LoadInitialAccessibilityTreeFromUrl(
embedded_test_server()->GetURL(html_file_path), accessibility_mode);
}
void LoadInitialAccessibilityTreeFromHtml(
const std::string& html,
ui::AXMode accessibility_mode = ui::kAXModeComplete) {
LoadInitialAccessibilityTreeFromUrl(
GURL("data:text/html," + base::EscapeQueryParamValue(html, false)),
accessibility_mode);
}
BrowserAccessibilityManager* GetManagerAndAssertNonNull() {
auto GetManagerAndAssertNonNull =
[this](BrowserAccessibilityManager** result) {
WebContentsImpl* web_contents_impl =
static_cast<WebContentsImpl*>(shell()->web_contents());
ASSERT_NE(nullptr, web_contents_impl);
BrowserAccessibilityManager* browser_accessibility_manager =
web_contents_impl->GetRootBrowserAccessibilityManager();
ASSERT_NE(nullptr, browser_accessibility_manager);
*result = browser_accessibility_manager;
};
BrowserAccessibilityManager* browser_accessibility_manager;
GetManagerAndAssertNonNull(&browser_accessibility_manager);
return browser_accessibility_manager;
}
BrowserAccessibility* GetRootAndAssertNonNull() {
auto GetRootAndAssertNonNull = [this](BrowserAccessibility** result) {
BrowserAccessibility* root_browser_accessibility =
GetManagerAndAssertNonNull()->GetBrowserAccessibilityRoot();
ASSERT_NE(nullptr, result);
*result = root_browser_accessibility;
};
BrowserAccessibility* root_browser_accessibility;
GetRootAndAssertNonNull(&root_browser_accessibility);
return root_browser_accessibility;
}
BrowserAccessibility* FindNode(ax::mojom::Role role,
const std::string& name_or_value) {
return FindNodeInSubtree(*GetRootAndAssertNonNull(), role, name_or_value);
}
void GetTextProviderFromTextNode(
ComPtr<ITextProvider>& text_provider,
BrowserAccessibility* target_browser_accessibility) {
auto* provider_simple =
ToBrowserAccessibilityWin(target_browser_accessibility)->GetCOM();
ASSERT_NE(nullptr, provider_simple);
EXPECT_HRESULT_SUCCEEDED(
provider_simple->GetPatternProvider(UIA_TextPatternId, &text_provider));
ASSERT_NE(nullptr, text_provider.Get());
}
private:
BrowserAccessibility* FindNodeInSubtree(BrowserAccessibility& node,
ax::mojom::Role role,
const std::string& name_or_value) {
const std::string& name =
node.GetStringAttribute(ax::mojom::StringAttribute::kName);
// Note that in the case of a text field,
// "BrowserAccessibility::GetValueForControl" has the added functionality
// of computing the value of an ARIA text box from its inner text.
//
// <div contenteditable="true" role="textbox">Hello world.</div>
// Will expose no HTML value attribute, but some screen readers, such as
// Jaws, VoiceOver and Talkback, require one to be computed.
const std::string value = base::UTF16ToUTF8(node.GetValueForControl());
if (node.GetRole() == role &&
(name == name_or_value || value == name_or_value)) {
return &node;
}
for (unsigned int i = 0; i < node.PlatformChildCount(); ++i) {
BrowserAccessibility* result =
FindNodeInSubtree(*node.PlatformGetChild(i), role, name_or_value);
if (result)
return result;
}
return nullptr;
}
base::test::ScopedFeatureList scoped_feature_list_{::features::kUiaProvider};
};
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
GetVisibleBounds) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
<div style='overflow: hidden; width: 10em; height: 2.1em;'>
<span style='white-space: pre-line;'>AAA BBB
CCCCCC
DDDDDD</span>
</div>
</body>
</html>
)HTML"));
auto* node =
FindNode(ax::mojom::Role::kStaticText, "AAA BBB\nCCCCCC\nDDDDDD");
ASSERT_NE(nullptr, node);
EXPECT_TRUE(node->IsLeaf());
EXPECT_EQ(0u, node->PlatformChildCount());
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, node);
base::win::ScopedSafearray text_provider_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(text_provider_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(text_provider_ranges.Get(), 2U);
ITextRangeProvider** array_data;
ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
text_provider_ranges.Get(), reinterpret_cast<void**>(&array_data)));
EXPECT_UIA_TEXTRANGE_EQ(array_data[0], L"AAA BBB");
EXPECT_UIA_TEXTRANGE_EQ(array_data[1], L"CCCCCC");
ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(text_provider_ranges.Get()));
text_provider_ranges.Reset();
}
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
GetVisibleRangesPositionsOnLeafNodes) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
<div contenteditable="true" role="textbox" aria-label="text">
<div><span>one two</span></div>
<div><span>three four</span></div>
<div><span>five six</span></div>
</div>
</body>
</html>
)HTML"));
auto* node =
FindNode(ax::mojom::Role::kTextField, "text");
ASSERT_NE(nullptr, node);
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, node);
base::win::ScopedSafearray text_provider_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(text_provider_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(text_provider_ranges.Get(), 3U);
ITextRangeProvider** array_data;
ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
text_provider_ranges.Get(), reinterpret_cast<void**>(&array_data)));
EXPECT_UIA_TEXTRANGE_EQ(array_data[0], L"one two");
EXPECT_UIA_TEXTRANGE_EQ(array_data[1], L"three four");
EXPECT_UIA_TEXTRANGE_EQ(array_data[2], L"five six");
ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(text_provider_ranges.Get()));
text_provider_ranges.Reset();
}
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
FindTextOnRangesReturnedByGetVisibleRanges) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
<div contenteditable="true" role="textbox" aria-label="text">
<div><span>one two</span></div>
<div><span>three four</span></div>
<div><span>five six</span></div>
</div>
</body>
</html>
)HTML"));
auto* node =
FindNode(ax::mojom::Role::kTextField, "text");
ASSERT_NE(nullptr, node);
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, node);
base::win::ScopedSafearray text_provider_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(text_provider_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(text_provider_ranges.Get(), 3U);
ITextRangeProvider** array_data;
ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
text_provider_ranges.Get(), reinterpret_cast<void**>(&array_data)));
EXPECT_UIA_TEXTRANGE_EQ(array_data[0], L"one two");
EXPECT_UIA_TEXTRANGE_EQ(array_data[1], L"three four");
EXPECT_UIA_TEXTRANGE_EQ(array_data[2], L"five six");
{
base::win::ScopedBstr find_string(L"two");
Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
EXPECT_HRESULT_SUCCEEDED(array_data[0]->FindText(
find_string.Get(), false, false, &text_range_provider_found));
ASSERT_TRUE(text_range_provider_found.Get());
}
{
base::win::ScopedBstr find_string(L"three");
Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
EXPECT_HRESULT_SUCCEEDED(array_data[1]->FindText(
find_string.Get(), false, false, &text_range_provider_found));
ASSERT_TRUE(text_range_provider_found.Get());
}
{
base::win::ScopedBstr find_string(L"five six");
Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
EXPECT_HRESULT_SUCCEEDED(array_data[2]->FindText(
find_string.Get(), false, false, &text_range_provider_found));
ASSERT_TRUE(text_range_provider_found.Get());
}
ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(text_provider_ranges.Get()));
text_provider_ranges.Reset();
}
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
GetVisibleRangesInContentEditable) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
<div contenteditable="true">
<p>hello</p>
</div>
</body>
</html>
)HTML"));
auto* gc_node = FindNode(ax::mojom::Role::kGenericContainer, "hello");
ASSERT_NE(nullptr, gc_node);
EXPECT_EQ(1u, gc_node->PlatformChildCount());
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, gc_node);
base::win::ScopedSafearray text_provider_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(text_provider_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(text_provider_ranges.Get(), 1U);
ITextRangeProvider** array_data;
ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
text_provider_ranges.Get(), reinterpret_cast<void**>(&array_data)));
// If the `embedded_object_character` was being exposed, the search for this
// string would fail.
// We have to use `FindText` instead of the `EXPECT_UIA_TEXTRANGE_EQ` macro
// since that macro uses `GetText` API which hardcodes the
// `AXEmbeddedObjectCharacter` to be exposed, which then in this case would
// mess up the text range. Filing a bug for `GetText`. CRBug: 1445692
base::win::ScopedBstr find_string(L"hello");
Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
EXPECT_HRESULT_SUCCEEDED(array_data[0]->FindText(
find_string.Get(), false, false, &text_range_provider_found));
ASSERT_TRUE(text_range_provider_found.Get());
ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(text_provider_ranges.Get()));
text_provider_ranges.Reset();
}
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
GetVisibleRangesForTextSlightlyOutsideContainer) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
<div role='textbox' contenteditable="true" style='height: 10px;'>
<span style='height:20px; display:inline-block'>hello</span>
</div>
</body>
</html>
)HTML"));
auto* gc_node = FindNode(ax::mojom::Role::kTextField, "hello");
ASSERT_NE(nullptr, gc_node);
EXPECT_EQ(1u, gc_node->PlatformChildCount());
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, gc_node);
base::win::ScopedSafearray text_provider_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(text_provider_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(text_provider_ranges.Get(), 1U);
ITextRangeProvider** array_data;
ASSERT_HRESULT_SUCCEEDED(::SafeArrayAccessData(
text_provider_ranges.Get(), reinterpret_cast<void**>(&array_data)));
EXPECT_UIA_TEXTRANGE_EQ(array_data[0], L"hello");
ASSERT_HRESULT_SUCCEEDED(::SafeArrayUnaccessData(text_provider_ranges.Get()));
text_provider_ranges.Reset();
}
IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest, GetVisibleRangesRefCount) {
LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
<!DOCTYPE html>
<html>
<body>
hello
</body>
</html>
)HTML"));
auto* text_node = FindNode(ax::mojom::Role::kStaticText, "hello");
ASSERT_NE(nullptr, text_node);
ComPtr<ITextProvider> text_provider;
GetTextProviderFromTextNode(text_provider, text_node);
base::win::ScopedSafearray visible_ranges;
EXPECT_HRESULT_SUCCEEDED(
text_provider->GetVisibleRanges(visible_ranges.Receive()));
ASSERT_UIA_SAFEARRAY_OF_TEXTRANGEPROVIDER(visible_ranges.Get(), 1U);
LONG index = 0;
ComPtr<ITextRangeProvider> text_range_provider;
EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement(
visible_ranges.Get(), &index, static_cast<void**>(&text_range_provider)));
// Validate that there was only one reference to the `text_range_provider`.
ASSERT_EQ(1U, text_range_provider->Release());
// This is needed to avoid calling SafeArrayDestroy from SafeArray's dtor when
// exiting the scope, which would crash trying to release the already
// destroyed `text_range_provider`.
visible_ranges.Release();
}
} // namespace content