blob: bf1f9c1784d6225a03f77f3cbcf58f75dd4cb3f9 [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 "third_party/blink/renderer/modules/accessibility/testing/accessibility_selection_test.h"
#include <algorithm>
#include <iterator>
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/public/platform/file_path_conversion.h"
#include "third_party/blink/renderer/core/dom/character_data.h"
#include "third_party/blink/renderer/core/dom/container_node.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/position.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/ax_position.h"
#include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/shared_buffer.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/vector.h"
namespace blink {
namespace {
constexpr char kSelectionTestsRelativePath[] = "selection/";
constexpr char kTestFileSuffix[] = ".html";
constexpr char kLayoutNGSuffix[] = "-ax-layout-ng.txt";
constexpr char kLayoutNGDisabledSuffix[] = "-ax-layout-ng-disabled.txt";
constexpr char kAXTestExpectationSuffix[] = "-ax.txt";
// Serialize accessibility subtree to selection text.
// Adds a '^' at the selection anchor offset and a '|' at the focus offset.
class AXSelectionSerializer final {
STACK_ALLOCATED();
public:
explicit AXSelectionSerializer(const AXSelection& selection)
: tree_level_(0), selection_(selection) {}
~AXSelectionSerializer() = default;
std::string Serialize(const AXObject& subtree) {
if (!selection_.IsValid())
return {};
SerializeSubtree(subtree);
DCHECK_EQ(tree_level_, 0);
return builder_.ToString().Utf8();
}
private:
void HandleTextObject(const AXObject& text_object) {
builder_.Append('<');
builder_.Append(AXObject::InternalRoleName(text_object.RoleValue()));
builder_.Append(": ");
const String name = text_object.ComputedName() + ">\n";
const AXObject& base_container = *selection_.Base().ContainerObject();
const AXObject& extent_container = *selection_.Extent().ContainerObject();
if (base_container == text_object && extent_container == text_object) {
DCHECK(selection_.Base().IsTextPosition() &&
selection_.Extent().IsTextPosition());
const int base_offset = selection_.Base().TextOffset();
const int extent_offset = selection_.Extent().TextOffset();
if (base_offset == extent_offset) {
builder_.Append(name.Left(base_offset));
builder_.Append('|');
builder_.Append(name.Substring(base_offset));
return;
}
if (base_offset < extent_offset) {
builder_.Append(name.Left(base_offset));
builder_.Append('^');
builder_.Append(
name.Substring(base_offset, extent_offset - base_offset));
builder_.Append('|');
builder_.Append(name.Substring(extent_offset));
return;
}
builder_.Append(name.Left(extent_offset));
builder_.Append('|');
builder_.Append(
name.Substring(extent_offset, base_offset - extent_offset));
builder_.Append('^');
builder_.Append(name.Substring(base_offset));
return;
}
if (base_container == text_object) {
DCHECK(selection_.Base().IsTextPosition());
const int base_offset = selection_.Base().TextOffset();
builder_.Append(name.Left(base_offset));
builder_.Append('^');
builder_.Append(name.Substring(base_offset));
return;
}
if (extent_container == text_object) {
DCHECK(selection_.Extent().IsTextPosition());
const int extent_offset = selection_.Extent().TextOffset();
builder_.Append(name.Left(extent_offset));
builder_.Append('|');
builder_.Append(name.Substring(extent_offset));
return;
}
builder_.Append(name);
}
void HandleObject(const AXObject& object) {
builder_.Append('<');
builder_.Append(AXObject::InternalRoleName(object.RoleValue()));
String name = object.ComputedName();
if (name.length()) {
builder_.Append(": ");
builder_.Append(name);
}
builder_.Append(">\n");
SerializeSubtree(object);
}
void HandleSelection(const AXPosition& position) {
if (!position.IsValid())
return;
if (selection_.Extent() == position) {
builder_.Append('|');
return;
}
if (selection_.Base() != position)
return;
builder_.Append('^');
}
void SerializeSubtree(const AXObject& subtree) {
if (!subtree.ChildCountIncludingIgnored()) {
// Though they are in this particular case both equivalent to an "after
// object" position, "Before children" and "after children" positions are
// still valid within empty subtrees.
const auto position = AXPosition::CreateFirstPositionInObject(subtree);
HandleSelection(position);
return;
}
for (const AXObject* child : subtree.ChildrenIncludingIgnored()) {
DCHECK(child);
const auto position = AXPosition::CreatePositionBeforeObject(*child);
HandleSelection(position);
++tree_level_;
builder_.Append(String::FromUTF8(std::string(tree_level_ * 2, '+')));
if (position.IsTextPosition()) {
HandleTextObject(*child);
} else {
HandleObject(*child);
}
--tree_level_;
}
// Handle any "after children" positions.
HandleSelection(AXPosition::CreateLastPositionInObject(subtree));
}
StringBuilder builder_;
int tree_level_;
AXSelection selection_;
};
// Deserializes an HTML snippet with or without selection markers to an
// |AXSelection| object. A '^' could be present at the selection anchor offset
// and a '|' at the focus offset. If multiple markers are present, the
// deserializer will return multiple |AXSelection| objects. If there are
// multiple markers, the first '|' in DOM order will be matched with the first
// '^' marker, the second '|' with the second '^', and so on. If there are more
// '|'s than '^'s or vice versa, the deserializer will DCHECK. If there are no
// markers, no |AXSelection| objects will be returned. We don't allow '^' and
// '|' markers to appear in anything other than the contents of an HTML node,
// e.g. they are not permitted in aria-labels.
class AXSelectionDeserializer final {
STACK_ALLOCATED();
public:
explicit AXSelectionDeserializer(AXObjectCacheImpl& cache)
: ax_object_cache_(&cache),
anchors_(MakeGarbageCollected<VectorOfPairs<Node, int>>()),
foci_(MakeGarbageCollected<VectorOfPairs<Node, int>>()) {}
~AXSelectionDeserializer() = default;
// Creates an accessibility tree rooted at the given HTML element from the
// provided HTML snippet and returns |AXSelection| objects that can select the
// parts of the tree indicated by the selection markers in the snippet.
const Vector<AXSelection> Deserialize(const std::string& html_snippet,
HTMLElement& element) {
element.setInnerHTML(String::FromUTF8(html_snippet));
element.GetDocument().View()->UpdateAllLifecyclePhasesForTest();
AXObject* root = ax_object_cache_->GetOrCreate(&element);
if (!root || root->IsDetached())
return {};
FindSelectionMarkers(*root);
DCHECK((foci_->size() == 1 && anchors_->size() == 0) ||
anchors_->size() == foci_->size())
<< "There should be an equal number of '^'s and '|'s in the HTML that "
"is being deserialized, or if caret placement is required, only a "
"single '|'.";
if (foci_->IsEmpty())
return {};
Vector<AXSelection> ax_selections;
if (anchors_->IsEmpty()) {
// Handle the case when there is just a single '|' marker representing the
// position of the caret.
DCHECK(foci_->at(0).first);
const Position caret(foci_->at(0).first, foci_->at(0).second);
const auto ax_caret = AXPosition::FromPosition(caret);
AXSelection::Builder builder;
ax_selections.push_back(
builder.SetBase(ax_caret).SetExtent(ax_caret).Build());
return ax_selections;
}
for (size_t i = 0; i < foci_->size(); ++i) {
DCHECK(anchors_->at(i).first);
const Position base(*anchors_->at(i).first, anchors_->at(i).second);
const auto ax_base = AXPosition::FromPosition(base);
DCHECK(foci_->at(i).first);
const Position extent(*foci_->at(i).first, foci_->at(i).second);
const auto ax_extent = AXPosition::FromPosition(extent);
AXSelection::Builder builder;
ax_selections.push_back(
builder.SetBase(ax_base).SetExtent(ax_extent).Build());
}
return ax_selections;
}
private:
void HandleCharacterData(const AXObject& text_object) {
auto* const node = To<CharacterData>(text_object.GetNode());
Vector<int> base_offsets;
Vector<int> extent_offsets;
unsigned number_of_markers = 0;
StringBuilder builder;
for (unsigned i = 0; i < node->length(); ++i) {
const UChar character = node->data()[i];
if (character == '^') {
base_offsets.push_back(static_cast<int>(i - number_of_markers));
++number_of_markers;
continue;
}
if (character == '|') {
extent_offsets.push_back(static_cast<int>(i - number_of_markers));
++number_of_markers;
continue;
}
builder.Append(character);
}
if (base_offsets.IsEmpty() && extent_offsets.IsEmpty())
return;
// Remove the markers, otherwise they would be duplicated if the AXSelection
// is re-serialized.
node->setData(builder.ToString());
node->GetDocument().View()->UpdateAllLifecyclePhasesForTest();
//
// Non-text selection.
//
if (node->ContainsOnlyWhitespaceOrEmpty()) {
// Since the text object contains only selection markers, this indicates
// that this is a request for a non-text selection.
Node* const parent = node->ParentOrShadowHostNode();
int index_in_parent = static_cast<int>(node->NodeIndex());
for (size_t i = 0; i < base_offsets.size(); ++i)
anchors_->emplace_back(parent, index_in_parent);
for (size_t i = 0; i < extent_offsets.size(); ++i)
foci_->emplace_back(parent, index_in_parent);
return;
}
//
// Text selection.
//
for (int base_offset : base_offsets)
anchors_->emplace_back(node, base_offset);
for (int extent_offset : extent_offsets)
foci_->emplace_back(node, extent_offset);
}
void HandleObject(const AXObject& object) {
// Make a copy of the children, because they may be cleared when a sibling
// is invalidated and calls SetNeedsToUpdateChildren() on the parent.
const auto children = object.ChildrenIncludingIgnored();
for (const AXObject* child : children) {
DCHECK(child);
FindSelectionMarkers(*child);
}
}
void FindSelectionMarkers(const AXObject& root) {
const Node* node = root.GetNode();
if (node && node->IsCharacterDataNode()) {
HandleCharacterData(root);
// |root| will need to be detached and replaced with an updated AXObject.
return;
}
HandleObject(root);
}
Persistent<AXObjectCacheImpl> const ax_object_cache_;
// Pairs of anchor nodes + anchor offsets.
Persistent<VectorOfPairs<Node, int>> anchors_;
// Pairs of focus nodes + focus offsets.
Persistent<VectorOfPairs<Node, int>> foci_;
};
} // namespace
AccessibilitySelectionTest::AccessibilitySelectionTest(
LocalFrameClient* local_frame_client)
: AccessibilityTest(local_frame_client) {}
void AccessibilitySelectionTest::SetUp() {
AccessibilityTest::SetUp();
RuntimeEnabledFeatures::SetAccessibilityExposeHTMLElementEnabled(true);
}
std::string AccessibilitySelectionTest::GetCurrentSelectionText() const {
const SelectionInDOMTree selection =
GetFrame().Selection().GetSelectionInDOMTree();
const auto ax_selection = AXSelection::FromSelection(selection);
return GetSelectionText(ax_selection);
}
std::string AccessibilitySelectionTest::GetSelectionText(
const AXSelection& selection) const {
const AXObject* root = GetAXRootObject();
if (!root || root->IsDetached())
return {};
return AXSelectionSerializer(selection).Serialize(*root);
}
std::string AccessibilitySelectionTest::GetSelectionText(
const AXSelection& selection,
const AXObject& subtree) const {
return AXSelectionSerializer(selection).Serialize(subtree);
}
AXSelection AccessibilitySelectionTest::SetSelectionText(
const std::string& selection_text) const {
HTMLElement* body = GetDocument().body();
if (!body)
return AXSelection::Builder().Build();
const Vector<AXSelection> ax_selections =
AXSelectionDeserializer(GetAXObjectCache())
.Deserialize(selection_text, *body);
if (ax_selections.IsEmpty())
return AXSelection::Builder().Build();
return ax_selections.front();
}
AXSelection AccessibilitySelectionTest::SetSelectionText(
const std::string& selection_text,
HTMLElement& element) const {
const Vector<AXSelection> ax_selections =
AXSelectionDeserializer(GetAXObjectCache())
.Deserialize(selection_text, element);
if (ax_selections.IsEmpty())
return AXSelection::Builder().Build();
return ax_selections.front();
}
void AccessibilitySelectionTest::RunSelectionTest(
const std::string& test_name,
const std::string& suffix) const {
static const std::string separator_line = '\n' + std::string(80, '=') + '\n';
const String relative_path = String::FromUTF8(kSelectionTestsRelativePath) +
String::FromUTF8(test_name);
const String test_path = test::AccessibilityTestDataPath(relative_path);
const String test_file = test_path + String::FromUTF8(kTestFileSuffix);
scoped_refptr<SharedBuffer> test_file_buffer = test::ReadFromFile(test_file);
auto test_file_chars = test_file_buffer->CopyAs<Vector<char>>();
std::string test_file_contents;
std::copy(test_file_chars.begin(), test_file_chars.end(),
std::back_inserter(test_file_contents));
ASSERT_FALSE(test_file_contents.empty())
<< "Test file cannot be empty.\n"
<< test_file.Utf8()
<< "\nDid you forget to add a data dependency to the BUILD file?";
const String ax_file =
test_path +
String::FromUTF8(suffix.empty() ? kAXTestExpectationSuffix : suffix);
scoped_refptr<SharedBuffer> ax_file_buffer = test::ReadFromFile(ax_file);
auto ax_file_chars = ax_file_buffer->CopyAs<Vector<char>>();
std::string ax_file_contents;
std::copy(ax_file_chars.begin(), ax_file_chars.end(),
std::back_inserter(ax_file_contents));
ASSERT_FALSE(ax_file_contents.empty())
<< "Expectations file cannot be empty.\n"
<< ax_file.Utf8()
<< "\nDid you forget to add a data dependency to the BUILD file?";
HTMLElement* body = GetDocument().body();
ASSERT_NE(nullptr, body);
Vector<AXSelection> ax_selections =
AXSelectionDeserializer(GetAXObjectCache())
.Deserialize(test_file_contents, *body);
std::string actual_ax_file_contents;
for (auto& ax_selection : ax_selections) {
ax_selection.Select();
actual_ax_file_contents += separator_line;
actual_ax_file_contents += ax_selection.ToString().Utf8();
actual_ax_file_contents += separator_line;
actual_ax_file_contents += GetCurrentSelectionText();
}
EXPECT_EQ(ax_file_contents, actual_ax_file_contents);
// Uncomment these lines to write the output to the expectations file.
// TODO(dmazzoni): make this a command-line parameter.
// if (ax_file_contents != actual_ax_file_contents)
// base::WriteFile(WebStringToFilePath(ax_file), actual_ax_file_contents);
}
ParameterizedAccessibilitySelectionTest::
ParameterizedAccessibilitySelectionTest(
LocalFrameClient* local_frame_client)
: ScopedLayoutNGForTest(GetParam()),
AccessibilitySelectionTest(local_frame_client) {}
void ParameterizedAccessibilitySelectionTest::RunSelectionTest(
const std::string& test_name) const {
std::string suffix =
LayoutNGEnabled() ? kLayoutNGSuffix : kLayoutNGDisabledSuffix;
AccessibilitySelectionTest::RunSelectionTest(test_name, suffix);
}
} // namespace blink