blob: cd07323d85521c66767fa0533d6f94b6376dad09 [file] [log] [blame]
// Copyright 2014 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 "content/browser/accessibility/dump_accessibility_browsertest_base.h"
#include <set>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/strings/string16.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_command_line.h"
#include "base/threading/thread_restrictions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "build/build_config.h"
#include "content/browser/accessibility/accessibility_event_recorder.h"
#include "content/browser/accessibility/browser_accessibility.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/content_switches.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/dump_accessibility_test_helper.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/base/ui_base_features.h"
namespace content {
namespace {
// Searches recursively and returns true if an accessibility node is found
// that represents a fully loaded web document with the given url.
bool AccessibilityTreeContainsLoadedDocWithUrl(BrowserAccessibility* node,
const std::string& url) {
if (node->GetRole() == ax::mojom::Role::kRootWebArea &&
node->GetStringAttribute(ax::mojom::StringAttribute::kUrl) == url) {
// Ensure the doc has finished loading and has a non-zero size.
return node->manager()->GetTreeData().loaded &&
(node->GetData().relative_bounds.bounds.width() > 0 &&
node->GetData().relative_bounds.bounds.height() > 0);
}
if (node->GetRole() == ax::mojom::Role::kWebArea &&
node->GetStringAttribute(ax::mojom::StringAttribute::kUrl) == url) {
// Ensure the doc has finished loading.
return node->manager()->GetTreeData().loaded;
}
for (unsigned i = 0; i < node->PlatformChildCount(); i++) {
if (AccessibilityTreeContainsLoadedDocWithUrl(node->PlatformGetChild(i),
url)) {
return true;
}
}
return false;
}
} // namespace
typedef AccessibilityTreeFormatter::PropertyFilter PropertyFilter;
typedef AccessibilityTreeFormatter::NodeFilter NodeFilter;
DumpAccessibilityTestBase::DumpAccessibilityTestBase()
: formatter_factory_(nullptr),
event_recorder_factory_(nullptr),
enable_accessibility_after_navigating_(false) {}
DumpAccessibilityTestBase::~DumpAccessibilityTestBase() {}
void DumpAccessibilityTestBase::SetUpCommandLine(
base::CommandLine* command_line) {
IsolateAllSitesForTesting(command_line);
// Each test pass might require custom command-line setup
auto passes = AccessibilityTreeFormatter::GetTestPasses();
size_t current_pass = GetParam();
CHECK_LT(current_pass, passes.size());
if (passes[current_pass].set_up_command_line)
passes[current_pass].set_up_command_line(command_line);
}
void DumpAccessibilityTestBase::SetUpOnMainThread() {
host_resolver()->AddRule("*", "127.0.0.1");
SetupCrossSiteRedirector(embedded_test_server());
ASSERT_TRUE(embedded_test_server()->Start());
}
void DumpAccessibilityTestBase::SetUp() {
std::vector<base::Feature> enabled_features;
std::vector<base::Feature> disabled_features;
ChooseFeatures(&enabled_features, &disabled_features);
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
// The <input type="color"> popup tested in
// AccessibilityInputColorWithPopupOpen requires the ability to read pixels
// from a Canvas, so we need to be able to produce pixel output.
EnablePixelOutput();
ContentBrowserTest::SetUp();
}
void DumpAccessibilityTestBase::ChooseFeatures(
std::vector<base::Feature>* enabled_features,
std::vector<base::Feature>* disabled_features) {
// Enable exposing ARIA Annotation roles.
// TODO(aleventhal) Remove when we completely remove runtime flag around m83.
// enabled_features.emplace_back(
// features::kEnableAccessibilityExposeARIAAnnotations);
// Enable exposing "display: none" nodes to the browser process for testing.
enabled_features->emplace_back(
features::kEnableAccessibilityExposeDisplayNone);
enabled_features->emplace_back(blink::features::kPortals);
// TODO(dmazzoni): DumpAccessibilityTree expectations are based on the
// assumption that the accessibility labels feature is off. (There are
// also several tests that explicitly enable the feature.) It'd be better
// if DumpAccessibilityTree tests assumed that the feature is on by
// default instead. http://crbug.com/940330
disabled_features->emplace_back(features::kExperimentalAccessibilityLabels);
}
base::string16
DumpAccessibilityTestBase::DumpUnfilteredAccessibilityTreeAsString() {
std::unique_ptr<AccessibilityTreeFormatter> formatter(formatter_factory_());
std::vector<PropertyFilter> property_filters;
property_filters.push_back(
PropertyFilter(base::ASCIIToUTF16("*"), PropertyFilter::ALLOW));
formatter->SetPropertyFilters(property_filters);
formatter->set_show_ids(true);
base::string16 ax_tree_dump;
formatter->FormatAccessibilityTreeForTesting(
GetRootAccessibilityNode(shell()->web_contents()), &ax_tree_dump);
return ax_tree_dump;
}
void DumpAccessibilityTestBase::ParseHtmlForExtraDirectives(
const std::string& test_html,
std::vector<std::string>* no_load_expected,
std::vector<std::string>* wait_for,
std::vector<std::string>* execute,
std::vector<std::string>* run_until,
std::vector<std::string>* default_action_on) {
for (const std::string& line : base::SplitString(
test_html, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL)) {
const std::string& allow_empty_str = formatter_->GetAllowEmptyString();
const std::string& allow_str = formatter_->GetAllowString();
const std::string& deny_str = formatter_->GetDenyString();
const std::string& deny_node_str = formatter_->GetDenyNodeString();
const std::string& no_load_expected_str = "@NO-LOAD-EXPECTED:";
const std::string& wait_str = "@WAIT-FOR:";
const std::string& execute_str = "@EXECUTE-AND-WAIT-FOR:";
const std::string& until_str = "@RUN-UNTIL-EVENT:";
const std::string& default_action_on_str = "@DEFAULT-ACTION-ON:";
if (base::StartsWith(line, allow_empty_str, base::CompareCase::SENSITIVE)) {
property_filters_.push_back(
PropertyFilter(base::UTF8ToUTF16(line.substr(allow_empty_str.size())),
PropertyFilter::ALLOW_EMPTY));
} else if (base::StartsWith(line, allow_str,
base::CompareCase::SENSITIVE)) {
property_filters_.push_back(
PropertyFilter(base::UTF8ToUTF16(line.substr(allow_str.size())),
PropertyFilter::ALLOW));
} else if (base::StartsWith(line, deny_str, base::CompareCase::SENSITIVE)) {
property_filters_.push_back(
PropertyFilter(base::UTF8ToUTF16(line.substr(deny_str.size())),
PropertyFilter::DENY));
} else if (base::StartsWith(line, deny_node_str,
base::CompareCase::SENSITIVE)) {
const auto& node_filter = line.substr(deny_node_str.size());
const auto& parts = base::SplitString(
node_filter, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Silently skip over parsing errors like the rest of the enclosing code.
if (parts.size() == 2) {
node_filters_.push_back(
NodeFilter(parts[0], base::UTF8ToUTF16(parts[1])));
}
} else if (base::StartsWith(line, no_load_expected_str,
base::CompareCase::SENSITIVE)) {
no_load_expected->push_back(line.substr(no_load_expected_str.size()));
} else if (base::StartsWith(line, wait_str, base::CompareCase::SENSITIVE)) {
wait_for->push_back(line.substr(wait_str.size()));
} else if (base::StartsWith(line, execute_str,
base::CompareCase::SENSITIVE)) {
execute->push_back(line.substr(execute_str.size()));
} else if (base::StartsWith(line, until_str,
base::CompareCase::SENSITIVE)) {
run_until->push_back(line.substr(until_str.size()));
} else if (base::StartsWith(line, default_action_on_str,
base::CompareCase::SENSITIVE)) {
default_action_on->push_back(line.substr(default_action_on_str.size()));
}
}
}
void DumpAccessibilityTestBase::RunTest(const base::FilePath file_path,
const char* file_dir) {
// Get all the tree formatters; the test is run independently on each one.
auto formatters = AccessibilityTreeFormatter::GetTestPasses();
auto event_recorders = AccessibilityEventRecorder::GetTestPasses();
CHECK(event_recorders.size() == formatters.size());
// The current test number is supplied as a test parameter.
size_t current_pass = GetParam();
CHECK_LT(current_pass, formatters.size());
CHECK_EQ(std::string(formatters[current_pass].name),
std::string(event_recorders[current_pass].name));
formatter_factory_ = formatters[current_pass].create_formatter;
event_recorder_factory_ = event_recorders[current_pass].create_recorder;
RunTestForPlatform(file_path, file_dir);
formatter_factory_ = nullptr;
event_recorder_factory_ = nullptr;
}
void DumpAccessibilityTestBase::RunTestForPlatform(
const base::FilePath file_path,
const char* file_dir) {
formatter_ = formatter_factory_();
DumpAccessibilityTestHelper test_helper(formatter_.get());
// Disable the "hot tracked" state (set when the mouse is hovering over
// an object) because it makes test output change based on the mouse position.
BrowserAccessibilityStateImpl::GetInstance()
->set_disable_hot_tracking_for_testing(true);
// Normally some accessibility events that would be fired are suppressed or
// delayed, depending on what has focus or the type of event. For testing,
// we want all events to fire immediately to make tests predictable and not
// flaky.
BrowserAccessibilityManager::NeverSuppressOrDelayEventsForTesting();
EXPECT_TRUE(NavigateToURL(shell(), GURL(url::kAboutBlankURL)));
// Exit without running the test if we can't find an expectation file.
// This is used to skip certain tests on certain platforms.
// We have to check for this in advance in order to avoid waiting on a
// WAIT-FOR directive in the source file that's looking for something not
// supported on the current platform.
base::FilePath expected_file = test_helper.GetExpectationFilePath(file_path);
if (expected_file.empty()) {
LOG(INFO) << "No expectation file present, ignoring test on this "
"platform.";
return;
}
base::Optional<std::vector<std::string>> expected_lines =
test_helper.LoadExpectationFile(expected_file);
if (!expected_lines) {
LOG(INFO) << "Skipping this test on this platform.";
return;
}
std::string html_contents;
{
base::ScopedAllowBlockingForTesting allow_blocking;
// Exit without running the test if the source file is missing, since that
// was the behavior prior to http://crrev.com/c/1661175.
// It would be preferable if we were to fail the test instead.
// http://crbug.com/975830
if (!base::ReadFileToString(file_path, &html_contents)) {
LOG(INFO) << "File not found: " << file_path.LossyDisplayName();
LOG(INFO) << "Skipping test.";
return;
}
}
// Parse filters and other directives in the test file.
std::vector<std::string> no_load_expected;
std::vector<std::string> wait_for;
std::vector<std::string> execute;
std::vector<std::string> run_until;
std::vector<std::string> default_action_on;
property_filters_.clear();
node_filters_.clear();
formatter_->AddDefaultFilters(&property_filters_);
AddDefaultFilters(&property_filters_);
ParseHtmlForExtraDirectives(html_contents, &no_load_expected, &wait_for,
&execute, &run_until, &default_action_on);
// Get the test URL.
GURL url(embedded_test_server()->GetURL("/" + std::string(file_dir) + "/" +
file_path.BaseName().MaybeAsASCII()));
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
if (enable_accessibility_after_navigating_ &&
web_contents->GetAccessibilityMode().is_mode_off()) {
// Load the url, then enable accessibility.
EXPECT_TRUE(NavigateToURL(shell(), url));
AccessibilityNotificationWaiter accessibility_waiter(
web_contents, ui::kAXModeComplete, ax::mojom::Event::kNone);
accessibility_waiter.WaitForNotification();
} else {
// Enable accessibility, then load the test html and wait for the
// "load complete" AX event.
AccessibilityNotificationWaiter accessibility_waiter(
web_contents, ui::kAXModeComplete, ax::mojom::Event::kLoadComplete);
EXPECT_TRUE(NavigateToURL(shell(), url));
accessibility_waiter.WaitForNotification();
}
// Perform default action on any elements specified by the test.
for (const auto& str : default_action_on) {
AccessibilityNotificationWaiter waiter(shell()->web_contents(),
ui::kAXModeComplete,
ax::mojom::Event::kClicked);
BrowserAccessibility* action_element;
size_t parent_node_delimiter_index = str.find(",");
if (parent_node_delimiter_index != std::string::npos) {
auto node_name = str.substr(0, parent_node_delimiter_index);
auto parent_node_name = str.substr(parent_node_delimiter_index + 1);
BrowserAccessibility* parent_node = FindNode(parent_node_name);
DCHECK(parent_node) << "Parent node name provided but not found";
action_element = FindNode(node_name, parent_node);
} else {
action_element = FindNode(str);
}
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kDoDefault;
action_element->AccessibilityPerformAction(action_data);
waiter.WaitForNotification();
}
WaitForAXTreeLoaded(web_contents, no_load_expected, wait_for);
// Call the subclass to dump the output.
std::vector<std::string> actual_lines = Dump(run_until);
// Execute and wait for specified string
for (const auto& function_name : execute) {
DLOG(INFO) << "executing: " << function_name;
base::Value result =
ExecuteScriptAndGetValue(web_contents->GetMainFrame(), function_name);
const std::string& str = result.is_string() ? result.GetString() : "";
// If no string is specified, do not wait.
bool wait_for_string = str != "";
while (wait_for_string) {
// Loop until specified string is found.
base::string16 tree_dump = DumpUnfilteredAccessibilityTreeAsString();
if (base::UTF16ToUTF8(tree_dump).find(str) != std::string::npos) {
wait_for_string = false;
// Append an additional dump if the specified string was found.
std::vector<std::string> additional_dump = Dump(run_until);
actual_lines.emplace_back("=== Start Continuation ===");
actual_lines.insert(actual_lines.end(), additional_dump.begin(),
additional_dump.end());
break;
}
// Block until the next accessibility notification in any frame.
VLOG(1) << "Still waiting on this text to be found: " << str;
VLOG(1) << "Waiting until the next accessibility event";
AccessibilityNotificationWaiter accessibility_waiter(
web_contents, ui::AXMode(), ax::mojom::Event::kNone);
accessibility_waiter.WaitForNotification();
}
}
// Validate against the expectation file.
bool matches_expectation = test_helper.ValidateAgainstExpectation(
file_path, expected_file, actual_lines, *expected_lines);
EXPECT_TRUE(matches_expectation);
if (!matches_expectation)
OnDiffFailed();
}
void DumpAccessibilityTestBase::WaitForAXTreeLoaded(
WebContentsImpl* web_contents,
const std::vector<std::string>& no_load_expected,
const std::vector<std::string>& wait_for) {
// Get the url of every frame in the frame tree.
FrameTree* frame_tree = web_contents->GetFrameTree();
std::vector<std::string> all_frame_urls;
for (FrameTreeNode* node : frame_tree->Nodes()) {
// Ignore about:blank urls because of the case where a parent frame A
// has a child iframe B and it writes to the document using
// contentDocument.open() on the child frame B.
//
// In this scenario, B's contentWindow.location.href matches A's url,
// but B's url in the browser frame tree is still "about:blank".
//
// We also ignore frame tree nodes created for portals in the outer
// WebContents as the node doesn't have a url set.
std::string url = node->current_url().spec();
// sometimes we expect a url to never load, in these cases, don't wait.
bool skip_url = false;
for (std::string no_load_url : no_load_expected) {
if (url.find(no_load_url) != std::string::npos) {
skip_url = true;
break;
}
}
if (!skip_url && url != url::kAboutBlankURL && !url.empty() &&
node->frame_owner_element_type() !=
blink::mojom::FrameOwnerElementType::kPortal) {
all_frame_urls.push_back(url);
}
}
// Wait for the accessibility tree to fully load for all frames,
// by searching for the WEB_AREA node in the accessibility tree
// with the url of each frame in our frame tree. Note that this
// doesn't support cases where there are two iframes with the
// exact same url. If all frames haven't loaded yet, set up a
// listener for accessibility events on any frame and block
// until the next one is received.
//
// If the original page has a @WAIT-FOR directive, don't break until
// the text we're waiting for appears in the full text dump of the
// accessibility tree, either.
for (;;) {
VLOG(1) << "Top of loop";
RenderFrameHostImpl* main_frame =
static_cast<RenderFrameHostImpl*>(web_contents->GetMainFrame());
BrowserAccessibilityManager* manager =
main_frame->browser_accessibility_manager();
if (manager) {
BrowserAccessibility* accessibility_root = manager->GetRoot();
// Check to see if all frames have loaded.
bool all_frames_loaded = true;
for (const auto& url : all_frame_urls) {
if (!AccessibilityTreeContainsLoadedDocWithUrl(accessibility_root,
url)) {
VLOG(1) << "Still waiting on this frame to load: " << url;
all_frames_loaded = false;
break;
}
}
// Check to see if the @WAIT-FOR text has appeared yet.
bool all_wait_for_strings_found = true;
base::string16 tree_dump = DumpUnfilteredAccessibilityTreeAsString();
for (const auto& str : wait_for) {
if (base::UTF16ToUTF8(tree_dump).find(str) == std::string::npos) {
VLOG(1) << "Still waiting on this text to be found: " << str;
all_wait_for_strings_found = false;
break;
}
}
// If all frames have loaded and the @WAIT-FOR text has appeared,
// we're done.
if (all_frames_loaded && all_wait_for_strings_found)
break;
}
// Block until the next accessibility notification in any frame.
VLOG(1) << "Waiting until the next accessibility event";
AccessibilityNotificationWaiter accessibility_waiter(
web_contents, ui::kAXModeComplete, ax::mojom::Event::kNone);
accessibility_waiter.WaitForNotification();
}
for (WebContents* inner_contents : web_contents->GetInnerWebContents()) {
WaitForAXTreeLoaded(static_cast<WebContentsImpl*>(inner_contents),
no_load_expected, std::vector<std::string>());
}
}
BrowserAccessibility* DumpAccessibilityTestBase::FindNode(
const std::string& name,
BrowserAccessibility* search_root) {
if (!search_root)
search_root = GetManager()->GetRoot();
CHECK(search_root);
BrowserAccessibility* node = FindNodeInSubtree(*search_root, name);
return node;
}
BrowserAccessibilityManager* DumpAccessibilityTestBase::GetManager() {
WebContentsImpl* web_contents =
static_cast<WebContentsImpl*>(shell()->web_contents());
return web_contents->GetRootBrowserAccessibilityManager();
}
BrowserAccessibility* DumpAccessibilityTestBase::FindNodeInSubtree(
BrowserAccessibility& node,
const std::string& name) {
if (node.GetStringAttribute(ax::mojom::StringAttribute::kName) == name)
return &node;
for (unsigned int i = 0; i < node.PlatformChildCount(); ++i) {
BrowserAccessibility* result =
FindNodeInSubtree(*node.PlatformGetChild(i), name);
if (result)
return result;
}
return nullptr;
}
} // namespace content