diff options
Diffstat (limited to 'chromium/ui/accessibility')
105 files changed, 5285 insertions, 1457 deletions
diff --git a/chromium/ui/accessibility/BUILD.gn b/chromium/ui/accessibility/BUILD.gn index a98305b4887..16e5316fdd2 100644 --- a/chromium/ui/accessibility/BUILD.gn +++ b/chromium/ui/accessibility/BUILD.gn @@ -20,10 +20,12 @@ if (is_win) { } mojom("ax_constants_mojo") { + generate_java = true sources = [ "ax_constants.mojom" ] } mojom_component("ax_enums_mojo") { + generate_java = true sources = [ "ax_enums.mojom" ] macro_prefix = "UI_ACCESSIBILITY_AX_MOJOM" @@ -77,17 +79,29 @@ component("ax_base") { "ax_tree_update_forward.h", ] + if (!is_chromeos_ash) { + sources += [ + "ax_enum_localization_util.cc", + "ax_enum_localization_util.h", + ] + } + public_deps = [ ":ax_constants_mojo", ":ax_enums_mojo", "//base", - "//base:i18n", - "//ui/base", "//ui/gfx", "//ui/gfx/geometry", - "//ui/strings", ] + if (!is_chromeos_ash) { + public_deps += [ + "//base:i18n", + "//ui/base", + "//ui/strings", + ] + } + deps = [ "//build:chromeos_buildflags" ] if (is_chromeos_ash) { @@ -141,6 +155,7 @@ component("accessibility") { "ax_tree.h", "ax_tree_combiner.cc", "ax_tree_combiner.h", + "ax_tree_manager.cc", "ax_tree_manager.h", "ax_tree_manager_base.cc", "ax_tree_manager_base.h", @@ -218,8 +233,12 @@ source_set("ax_assistant") { static_library("test_support") { testonly = true sources = [ + "platform/test_ax_tree_update.cc", + "platform/test_ax_tree_update.h", "test_ax_node_helper.cc", "test_ax_node_helper.h", + "test_ax_tree_update_json_reader.cc", + "test_ax_tree_update_json_reader.h", "tree_generator.cc", "tree_generator.h", ] @@ -290,8 +309,6 @@ test("accessibility_unittests") { "//ui/gfx:test_support", ] - data_deps = [ "//testing/buildbot/filters:accessibility_unittests_filters" ] - if (is_fuchsia) { sources += [ "platform/fuchsia/accessibility_bridge_fuchsia_unittest.cc", @@ -358,6 +375,32 @@ fuzzer_test("ax_table_fuzzer") { seed_corpus = "fuzz_corpus" } +fuzzer_test("ax_node_position_fuzzer") { + sources = [ + "ax_node_position_fuzzer.cc", + "ax_tree_fuzzer_util.cc", + "ax_tree_fuzzer_util.h", + ] + + deps = [ ":accessibility" ] +} + +if (is_win) { + fuzzer_test("ax_platform_node_textrangeprovider_win_fuzzer") { + sources = [ + "ax_tree_fuzzer_util.cc", + "ax_tree_fuzzer_util.h", + "platform/ax_platform_node_textrangeprovider_win_fuzzer.cc", + ] + + deps = [ + ":accessibility", + ":test_support", + "//base/test:test_support", + ] + } +} + test("accessibility_perftests") { testonly = true sources = [ "ax_node_position_perftest.cc" ] diff --git a/chromium/ui/accessibility/OWNERS b/chromium/ui/accessibility/OWNERS index cfdb21e52d3..23fbf83409b 100644 --- a/chromium/ui/accessibility/OWNERS +++ b/chromium/ui/accessibility/OWNERS @@ -1,5 +1,6 @@ abigailbklein@google.com aleventhal@chromium.org +benjamin.beaudry@microsoft.com dlibby@microsoft.com dtseng@chromium.org katie@chromium.org diff --git a/chromium/ui/accessibility/accessibility_features.cc b/chromium/ui/accessibility/accessibility_features.cc index aeaed89ea0f..fa22a30090a 100644 --- a/chromium/ui/accessibility/accessibility_features.cc +++ b/chromium/ui/accessibility/accessibility_features.cc @@ -79,6 +79,13 @@ bool IsAutoDisableAccessibilityEnabled() { return base::FeatureList::IsEnabled(::features::kAutoDisableAccessibility); } +const base::Feature kTextBasedAudioDescription{ + "TextBasedAudioDescription", base::FEATURE_DISABLED_BY_DEFAULT}; + +bool IsTextBasedAudioDescriptionEnabled() { + return base::FeatureList::IsEnabled(::features::kTextBasedAudioDescription); +} + #if BUILDFLAG(IS_WIN) const base::Feature kIChromeAccessible{"IChromeAccessible", base::FEATURE_DISABLED_BY_DEFAULT}; @@ -145,21 +152,13 @@ bool IsEnhancedNetworkVoicesEnabled() { return base::FeatureList::IsEnabled(::features::kEnhancedNetworkVoices); } -const base::Feature kAccessibilityOSSettingsReorganization{ - "AccessibilityOSSettingsReorganization", base::FEATURE_DISABLED_BY_DEFAULT}; - -bool IsAccessibilityOSSettingsReorganizationEnabled() { - return base::FeatureList::IsEnabled( - ::features::kAccessibilityOSSettingsReorganization); -} const base::Feature kAccessibilityOSSettingsVisibility{ - "AccessibilityOSSettingsVisibility", base::FEATURE_ENABLED_BY_DEFAULT}; + "AccessibilityOSSettingsVisibility", base::FEATURE_DISABLED_BY_DEFAULT}; bool IsAccessibilityOSSettingsVisibilityEnabled() { return base::FeatureList::IsEnabled( ::features::kAccessibilityOSSettingsVisibility); } - #endif // BUILDFLAG(IS_CHROMEOS_ASH) const base::Feature kAugmentExistingImageLabels{ @@ -215,7 +214,7 @@ bool IsScreenAIVisualAnnotationsEnabled() { } bool IsScreenAIServiceNeeded() { - return IsScreenAIVisualAnnotationsEnabled() || + return IsPdfOcrEnabled() || IsScreenAIVisualAnnotationsEnabled() || IsReadAnythingWithScreen2xEnabled(); } diff --git a/chromium/ui/accessibility/accessibility_features.h b/chromium/ui/accessibility/accessibility_features.h index cb7f3556d84..942d0e2d739 100644 --- a/chromium/ui/accessibility/accessibility_features.h +++ b/chromium/ui/accessibility/accessibility_features.h @@ -62,6 +62,14 @@ AX_BASE_EXPORT extern const base::Feature kAutoDisableAccessibility; // accessibility API usage in that time. AX_BASE_EXPORT bool IsAutoDisableAccessibilityEnabled(); +// Enables a setting that can turn on/off browser vocalization of 'descriptions' +// tracks. +AX_BASE_EXPORT extern const base::Feature kTextBasedAudioDescription; + +// Returns true if the setting to turn on text based audio descriptions is +// enabled. +AX_BASE_EXPORT bool IsTextBasedAudioDescriptionEnabled(); + #if BUILDFLAG(IS_WIN) // Enables an experimental Chrome-specific accessibility COM API AX_BASE_EXPORT extern const base::Feature kIChromeAccessible; @@ -118,13 +126,6 @@ AX_BASE_EXPORT extern const base::Feature kEnhancedNetworkVoices; // Returns true if network-based voices are enabled in Select-to-speak. AX_BASE_EXPORT bool IsEnhancedNetworkVoicesEnabled(); -// Enables improved Accessibility OS Settings reorganization. -AX_BASE_EXPORT extern const base::Feature - kAccessibilityOSSettingsReorganization; - -// Returns true if improved Accessibility OS Settings reorganization is enabled. -AX_BASE_EXPORT bool IsAccessibilityOSSettingsReorganizationEnabled(); - // Enables improved Accessibility OS Settings visibility. AX_BASE_EXPORT extern const base::Feature kAccessibilityOSSettingsVisibility; diff --git a/chromium/ui/accessibility/ax_common.h b/chromium/ui/accessibility/ax_common.h index 0153b455d7c..74b9575a938 100644 --- a/chromium/ui/accessibility/ax_common.h +++ b/chromium/ui/accessibility/ax_common.h @@ -19,7 +19,9 @@ // SANITIZER_CHECK's use case is severe, but recoverable situations that need // priority debugging. They trigger on Clusterfuzz, debug and sanitizer builds. -#if defined(AX_FAIL_FAST_BUILD) +// Prefer DCHECK() when enabled because it logs messages in the crash tool, +// unlike CHECK(). +#if defined(AX_FAIL_FAST_BUILD) && !DCHECK_IS_ON() #define SANITIZER_CHECK(val) CHECK(val) #define SANITIZER_CHECK_EQ(val1, val2) CHECK_EQ(val1, val2) #define SANITIZER_CHECK_NE(val1, val2) CHECK_NE(val1, val2) @@ -38,6 +40,6 @@ #define SANITIZER_CHECK_GE(val1, val2) DCHECK_GE(val1, val2) #define SANITIZER_CHECK_GT(val1, val2) DCHECK_GT(val1, val2) #define SANITIZER_NOTREACHED() NOTREACHED() -#endif // AX_FAIL_FAST_BUIL +#endif // AX_FAIL_FAST_BUILD && !DCHECK_IS_ON() #endif // UI_ACCESSIBILITY_AX_COMMON_H_ diff --git a/chromium/ui/accessibility/ax_computed_node_data.cc b/chromium/ui/accessibility/ax_computed_node_data.cc index cbb3f6f33e0..1a6fe8b8d77 100644 --- a/chromium/ui/accessibility/ax_computed_node_data.cc +++ b/chromium/ui/accessibility/ax_computed_node_data.cc @@ -25,22 +25,59 @@ AXComputedNodeData::AXComputedNodeData(const AXNode& node) : owner_(&node) {} AXComputedNodeData::~AXComputedNodeData() = default; int AXComputedNodeData::GetOrComputeUnignoredIndexInParent() const { - DCHECK(!owner_->IsIgnored()); + DCHECK(!owner_->IsIgnored()) + << "Ignored nodes cannot have an `unignored index in parent`.\n" + << *owner_; if (unignored_index_in_parent_) return *unignored_index_in_parent_; - if (const AXNode* unignored_parent = owner_->GetUnignoredParent()) { + if (const AXNode* unignored_parent = SlowGetUnignoredParent()) { + DCHECK_NE(unignored_parent->id(), kInvalidAXNodeID) + << "All nodes should have a valid ID.\n" + << *owner_; unignored_parent->GetComputedNodeData().ComputeUnignoredValues(); } else { // This should be the root node and, by convention, we assign it an // index-in-parent of 0. unignored_index_in_parent_ = 0; + unignored_parent_id_ = kInvalidAXNodeID; } return *unignored_index_in_parent_; } +AXNodeID AXComputedNodeData::GetOrComputeUnignoredParentID() const { + if (unignored_parent_id_) + return *unignored_parent_id_; + + if (const AXNode* unignored_parent = SlowGetUnignoredParent()) { + DCHECK_NE(unignored_parent->id(), kInvalidAXNodeID) + << "All nodes should have a valid ID.\n" + << *owner_; + unignored_parent_id_ = unignored_parent->id(); + } else { + // This should be the root node and, by convention, we assign it an + // index-in-parent of 0. + DCHECK(!owner_->GetParent()) + << "If `unignored_parent` is nullptr, then this should be the " + "rootnode, since in all trees the rootnode should be unignored.\n" + << *owner_; + unignored_index_in_parent_ = 0; + unignored_parent_id_ = kInvalidAXNodeID; + } + return *unignored_parent_id_; +} + +AXNode* AXComputedNodeData::GetOrComputeUnignoredParent() const { + DCHECK(owner_->tree()) + << "All nodes should be owned by an accessibility tree.\n" + << *owner_; + return owner_->tree()->GetFromId(GetOrComputeUnignoredParentID()); +} + int AXComputedNodeData::GetOrComputeUnignoredChildCount() const { - DCHECK(!owner_->IsIgnored()); + DCHECK(!owner_->IsIgnored()) + << "Ignored nodes cannot have an `unignored child count`.\n" + << *owner_; if (!unignored_child_count_) ComputeUnignoredValues(); return *unignored_child_count_; @@ -48,12 +85,20 @@ int AXComputedNodeData::GetOrComputeUnignoredChildCount() const { const std::vector<AXNodeID>& AXComputedNodeData::GetOrComputeUnignoredChildIDs() const { - DCHECK(!owner_->IsIgnored()); + DCHECK(!owner_->IsIgnored()) + << "Ignored nodes cannot have `unignored child IDs`.\n" + << *owner_; if (!unignored_child_ids_) ComputeUnignoredValues(); return *unignored_child_ids_; } +bool AXComputedNodeData::GetOrComputeIsDescendantOfPlatformLeaf() const { + if (!is_descendant_of_leaf_) + ComputeIsDescendantOfPlatformLeaf(); + return *is_descendant_of_leaf_; +} + bool AXComputedNodeData::HasOrCanComputeAttribute( const ax::mojom::StringAttribute attribute) const { if (owner_->data().HasStringAttribute(attribute)) @@ -235,12 +280,18 @@ int AXComputedNodeData::GetOrComputeTextContentLengthUTF16() const { } void AXComputedNodeData::ComputeUnignoredValues( + AXNodeID unignored_parent_id, int starting_index_in_parent) const { + DCHECK_GE(starting_index_in_parent, 0); // Reset any previously computed values. unignored_index_in_parent_ = absl::nullopt; + unignored_parent_id_ = absl::nullopt; unignored_child_count_ = absl::nullopt; unignored_child_ids_ = absl::nullopt; + AXNodeID unignored_parent_id_for_child = unignored_parent_id; + if (!owner_->IsIgnored()) + unignored_parent_id_for_child = owner_->id(); int unignored_child_count = 0; std::vector<AXNodeID> unignored_child_ids; for (auto iter = owner_->AllChildrenBegin(); iter != owner_->AllChildrenEnd(); @@ -250,7 +301,8 @@ void AXComputedNodeData::ComputeUnignoredValues( if (iter->IsIgnored()) { // Skip the ignored node and recursively look at its children. - computed_data.ComputeUnignoredValues(new_index_in_parent); + computed_data.ComputeUnignoredValues(unignored_parent_id_for_child, + new_index_in_parent); DCHECK(computed_data.unignored_child_count_); unignored_child_count += *computed_data.unignored_child_count_; DCHECK(computed_data.unignored_child_ids_); @@ -258,18 +310,50 @@ void AXComputedNodeData::ComputeUnignoredValues( computed_data.unignored_child_ids_->begin(), computed_data.unignored_child_ids_->end()); } else { + // Setting `unignored_index_in_parent_` and `unignored_parent_id_` is the + // responsibility of the parent node, since only the parent node can + // calculate these values. This is in contrast to `unignored_child_count_` + // and `unignored_child_ids_` that are only set if this method is called + // on the node itself. + computed_data.unignored_index_in_parent_ = new_index_in_parent; + if (unignored_parent_id_for_child != kInvalidAXNodeID) + computed_data.unignored_parent_id_ = unignored_parent_id_for_child; + ++unignored_child_count; unignored_child_ids.push_back(iter->id()); - computed_data.unignored_index_in_parent_ = new_index_in_parent; } } + if (unignored_parent_id != kInvalidAXNodeID) + unignored_parent_id_ = unignored_parent_id; // Ignored nodes store unignored child information in order to propagate it to - // their parents, but do not expose it directly. + // their parents, but do not expose it directly. The latter is guarded via a + // DCHECK. unignored_child_count_ = unignored_child_count; unignored_child_ids_ = unignored_child_ids; } +AXNode* AXComputedNodeData::SlowGetUnignoredParent() const { + AXNode* unignored_parent = owner_->GetParent(); + while (unignored_parent && unignored_parent->IsIgnored()) + unignored_parent = unignored_parent->GetParent(); + return unignored_parent; +} + +void AXComputedNodeData::ComputeIsDescendantOfPlatformLeaf() const { + is_descendant_of_leaf_ = false; + for (const AXNode* ancestor = GetOrComputeUnignoredParent(); ancestor; + ancestor = + ancestor->GetComputedNodeData().GetOrComputeUnignoredParent()) { + if (ancestor->GetComputedNodeData().is_descendant_of_leaf_.value_or( + false) || + ancestor->IsLeaf()) { + is_descendant_of_leaf_ = true; + return; + } + } +} + void AXComputedNodeData::ComputeLineOffsetsIfNeeded() const { if (line_starts_ || line_ends_) { DCHECK_EQ(line_starts_->size(), line_ends_->size()); @@ -392,7 +476,6 @@ std::string AXComputedNodeData::ComputeTextContentUTF8() const { if (owner_->IsLeaf() && !is_atomic_text_field_with_descendants) { switch (owner_->data().GetNameFrom()) { case ax::mojom::NameFrom::kNone: - case ax::mojom::NameFrom::kUninitialized: // The accessible name is not displayed on screen, e.g. aria-label, or is // not displayed directly inside the node, e.g. an associated label // element. diff --git a/chromium/ui/accessibility/ax_computed_node_data.h b/chromium/ui/accessibility/ax_computed_node_data.h index fe7d526683b..c7e82426111 100644 --- a/chromium/ui/accessibility/ax_computed_node_data.h +++ b/chromium/ui/accessibility/ax_computed_node_data.h @@ -15,6 +15,7 @@ #include "ui/accessibility/ax_enums.mojom-forward.h" #include "ui/accessibility/ax_export.h" #include "ui/accessibility/ax_node_data.h" +#include "ui/accessibility/ax_node_id_forward.h" namespace ui { @@ -37,6 +38,12 @@ class AX_EXPORT AXComputedNodeData final { // associated node is ignored. int GetOrComputeUnignoredIndexInParent() const; + // The lowest unignored parent. This value should be computed for all + // associated nodes, ignored and unignored. Only the rootnode should not have + // an unignored parent. + AXNodeID GetOrComputeUnignoredParentID() const; + AXNode* GetOrComputeUnignoredParent() const; + // If the associated node is unignored, i.e. exposed to the platform's // assistive software, the number of its children that are also unignored. // Naturally, this value is not defined when the associated node is ignored. @@ -46,6 +53,11 @@ class AX_EXPORT AXComputedNodeData final { // assistive software, the IDs of its children that are also unignored. const std::vector<AXNodeID>& GetOrComputeUnignoredChildIDs() const; + // Whether the associated node is a descendant of a platform leaf. The set of + // platform leaves are the lowest nodes that are exposed to the platform's + // assistive software. + bool GetOrComputeIsDescendantOfPlatformLeaf() const; + // Given an accessibility attribute, returns whether the attribute is // currently present in the node's data, or if it can always be computed on // demand. @@ -97,10 +109,20 @@ class AX_EXPORT AXComputedNodeData final { int GetOrComputeTextContentLengthUTF16() const; private: - // Computes and caches the `unignored_index_in_parent_`, + // Computes and caches the `unignored_index_in_parent_`, `unignored_parent_`, // `unignored_child_count_` and `unignored_child_ids_` for the associated // node. - void ComputeUnignoredValues(int starting_index_in_parent = 0) const; + void ComputeUnignoredValues(AXNodeID unignored_parent_id = kInvalidAXNodeID, + int starting_index_in_parent = 0) const; + + // Walks up the accessibility tree from the associated node until it finds the + // lowest unignored ancestor. + AXNode* SlowGetUnignoredParent() const; + + // Computes and caches (if not already in the cache) whether the associated + // node is a descendant of a platform leaf. The set of platform leaves are the + // lowest nodes that are exposed to the platform's assistive software. + void ComputeIsDescendantOfPlatformLeaf() const; // Computes and caches (if not already in the cache) the character offsets // where each line in the associated node's on-screen text starts and ends. @@ -126,8 +148,10 @@ class AX_EXPORT AXComputedNodeData final { const raw_ptr<const AXNode> owner_; mutable absl::optional<int> unignored_index_in_parent_; + mutable absl::optional<AXNodeID> unignored_parent_id_; mutable absl::optional<int> unignored_child_count_; mutable absl::optional<std::vector<AXNodeID>> unignored_child_ids_; + mutable absl::optional<bool> is_descendant_of_leaf_; mutable absl::optional<std::vector<int32_t>> line_starts_; mutable absl::optional<std::vector<int32_t>> line_ends_; mutable absl::optional<std::vector<int32_t>> sentence_starts_; diff --git a/chromium/ui/accessibility/ax_computed_node_data_unittest.cc b/chromium/ui/accessibility/ax_computed_node_data_unittest.cc index 5a5a60d15b9..c6e6e16d015 100644 --- a/chromium/ui/accessibility/ax_computed_node_data_unittest.cc +++ b/chromium/ui/accessibility/ax_computed_node_data_unittest.cc @@ -13,6 +13,7 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" +#include "ui/accessibility/ax_node_id_forward.h" #include "ui/accessibility/ax_position.h" #include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_data.h" @@ -159,6 +160,8 @@ using ::testing::StrEq; TEST_F(AXComputedNodeDataTest, UnignoredValues) { const AXNode* paragraph_0_node = root_node_->GetChildAtIndex(0); + const AXNode* static_text_0_0_ignored_node = + paragraph_0_node->GetChildAtIndex(0); const AXNode* paragraph_1_ignored_node = root_node_->GetChildAtIndex(1); const AXNode* static_text_1_0_node = paragraph_1_ignored_node->GetChildAtIndex(0); @@ -185,6 +188,65 @@ TEST_F(AXComputedNodeDataTest, UnignoredValues) { .GetOrComputeUnignoredIndexInParent()); EXPECT_EQ( + kInvalidAXNodeID, + root_node_->GetComputedNodeData().GetOrComputeUnignoredParentID()); + EXPECT_EQ(nullptr, + root_node_->GetComputedNodeData().GetOrComputeUnignoredParent()); + EXPECT_FALSE(root_node_->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), paragraph_0_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ( + root_node_, + paragraph_0_node->GetComputedNodeData().GetOrComputeUnignoredParent()); + EXPECT_FALSE(paragraph_0_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(paragraph_0_node->id(), + static_text_0_0_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(paragraph_0_node, + static_text_0_0_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_TRUE(static_text_0_0_ignored_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), paragraph_1_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, paragraph_1_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(paragraph_1_ignored_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), static_text_1_0_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, static_text_1_0_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(static_text_1_0_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), paragraph_2_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, paragraph_2_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(paragraph_2_ignored_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), link_2_0_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, link_2_0_ignored_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(link_2_0_ignored_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), static_text_2_0_0_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, static_text_2_0_0_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(static_text_2_0_0_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + EXPECT_EQ(root_node_->id(), static_text_2_0_1_node->GetComputedNodeData() + .GetOrComputeUnignoredParentID()); + EXPECT_EQ(root_node_, static_text_2_0_1_node->GetComputedNodeData() + .GetOrComputeUnignoredParent()); + EXPECT_FALSE(static_text_2_0_1_node->GetComputedNodeData() + .GetOrComputeIsDescendantOfPlatformLeaf()); + + EXPECT_EQ( 4, root_node_->GetComputedNodeData().GetOrComputeUnignoredChildCount()); EXPECT_EQ(0, paragraph_0_node->GetComputedNodeData() .GetOrComputeUnignoredChildCount()); @@ -267,7 +329,7 @@ TEST_F(AXComputedNodeDataTest, HasOrCanComputeAttribute) { TEST_F(AXComputedNodeDataTest, GetOrComputeAttribute) { // Embedded object behavior is dependant on platform. We manually set it to a // specific value so that test results are consistent across platforms. - testing::ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour( + ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour( AXEmbeddedObjectBehavior::kSuppressCharacter); // Line breaks should be inserted between each paragraph to mirror how HTML's @@ -449,7 +511,7 @@ TEST_F(AXComputedNodeDataTest, GetOrComputeAttribute) { TEST_F(AXComputedNodeDataTest, GetOrComputeTextContent) { // Embedded object behavior is dependant on platform. We manually set it to a // specific value so that test results are consistent across platforms. - testing::ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour( + ScopedAXEmbeddedObjectBehaviorSetter embedded_object_behaviour( AXEmbeddedObjectBehavior::kSuppressCharacter); EXPECT_THAT(root_node_->GetComputedNodeData() diff --git a/chromium/ui/accessibility/ax_enum_localization_util.cc b/chromium/ui/accessibility/ax_enum_localization_util.cc new file mode 100644 index 00000000000..ee31b0f5f7c --- /dev/null +++ b/chromium/ui/accessibility/ax_enum_localization_util.cc @@ -0,0 +1,40 @@ +// Copyright 2022 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 "ui/accessibility/ax_enum_localization_util.h" + +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/base/l10n/l10n_util.h" +#include "ui/strings/grit/ax_strings.h" + +namespace ui { + +std::string ToLocalizedString(ax::mojom::DefaultActionVerb action_verb) { + switch (action_verb) { + case ax::mojom::DefaultActionVerb::kNone: + return ""; + case ax::mojom::DefaultActionVerb::kActivate: + return l10n_util::GetStringUTF8(IDS_AX_ACTIVATE_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kCheck: + return l10n_util::GetStringUTF8(IDS_AX_CHECK_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kClick: + return l10n_util::GetStringUTF8(IDS_AX_CLICK_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kClickAncestor: + return l10n_util::GetStringUTF8(IDS_AX_CLICK_ANCESTOR_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kJump: + return l10n_util::GetStringUTF8(IDS_AX_JUMP_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kOpen: + return l10n_util::GetStringUTF8(IDS_AX_OPEN_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kPress: + return l10n_util::GetStringUTF8(IDS_AX_PRESS_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kSelect: + return l10n_util::GetStringUTF8(IDS_AX_SELECT_ACTION_VERB); + case ax::mojom::DefaultActionVerb::kUncheck: + return l10n_util::GetStringUTF8(IDS_AX_UNCHECK_ACTION_VERB); + } + + return ""; +} + +} // namespace ui diff --git a/chromium/ui/accessibility/ax_enum_localization_util.h b/chromium/ui/accessibility/ax_enum_localization_util.h new file mode 100644 index 00000000000..fce548b7f77 --- /dev/null +++ b/chromium/ui/accessibility/ax_enum_localization_util.h @@ -0,0 +1,21 @@ +// Copyright 2022 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. + +#ifndef UI_ACCESSIBILITY_AX_ENUM_LOCALIZATION_UTIL_H_ +#define UI_ACCESSIBILITY_AX_ENUM_LOCALIZATION_UTIL_H_ + +#include <string> + +#include "ui/accessibility/ax_base_export.h" +#include "ui/accessibility/ax_enums.mojom-forward.h" + +namespace ui { + +// Returns a localized string that corresponds to the name of the given action. +AX_BASE_EXPORT std::string ToLocalizedString( + ax::mojom::DefaultActionVerb action_verb); + +} // namespace ui + +#endif // UI_ACCESSIBILITY_AX_ENUM_LOCALIZATION_UTIL_H_ diff --git a/chromium/ui/accessibility/ax_enum_util.cc b/chromium/ui/accessibility/ax_enum_util.cc index 3a82bcb7f80..aee128d71f5 100644 --- a/chromium/ui/accessibility/ax_enum_util.cc +++ b/chromium/ui/accessibility/ax_enum_util.cc @@ -6,9 +6,6 @@ #include "ui/accessibility/ax_enums.mojom.h" -#include "ui/base/l10n/l10n_util.h" -#include "ui/strings/grit/ax_strings.h" - namespace ui { const char* ToString(ax::mojom::Event event) { @@ -750,33 +747,6 @@ const char* ToString(ax::mojom::DefaultActionVerb default_action_verb) { return ""; } -std::string ToLocalizedString(ax::mojom::DefaultActionVerb action_verb) { - switch (action_verb) { - case ax::mojom::DefaultActionVerb::kNone: - return ""; - case ax::mojom::DefaultActionVerb::kActivate: - return l10n_util::GetStringUTF8(IDS_AX_ACTIVATE_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kCheck: - return l10n_util::GetStringUTF8(IDS_AX_CHECK_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kClick: - return l10n_util::GetStringUTF8(IDS_AX_CLICK_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kClickAncestor: - return l10n_util::GetStringUTF8(IDS_AX_CLICK_ANCESTOR_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kJump: - return l10n_util::GetStringUTF8(IDS_AX_JUMP_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kOpen: - return l10n_util::GetStringUTF8(IDS_AX_OPEN_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kPress: - return l10n_util::GetStringUTF8(IDS_AX_PRESS_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kSelect: - return l10n_util::GetStringUTF8(IDS_AX_SELECT_ACTION_VERB); - case ax::mojom::DefaultActionVerb::kUncheck: - return l10n_util::GetStringUTF8(IDS_AX_UNCHECK_ACTION_VERB); - } - - return ""; -} - const char* ToString(ax::mojom::Mutation mutation) { switch (mutation) { case ax::mojom::Mutation::kNone: @@ -1552,8 +1522,6 @@ const char* ToString(ax::mojom::NameFrom name_from) { switch (name_from) { case ax::mojom::NameFrom::kNone: return "none"; - case ax::mojom::NameFrom::kUninitialized: - return "uninitialized"; case ax::mojom::NameFrom::kAttribute: return "attribute"; case ax::mojom::NameFrom::kAttributeExplicitlyEmpty: @@ -1581,6 +1549,8 @@ const char* ToString(ax::mojom::DescriptionFrom description_from) { return "none"; case ax::mojom::DescriptionFrom::kAriaDescription: return "ariaDescription"; + case ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty: + return "attributeExplicitlyEmpty"; case ax::mojom::DescriptionFrom::kButtonLabel: return "buttonLabel"; case ax::mojom::DescriptionFrom::kPopupElement: diff --git a/chromium/ui/accessibility/ax_enum_util.h b/chromium/ui/accessibility/ax_enum_util.h index f3c4605e9de..0bc02e0ad89 100644 --- a/chromium/ui/accessibility/ax_enum_util.h +++ b/chromium/ui/accessibility/ax_enum_util.h @@ -36,10 +36,6 @@ AX_BASE_EXPORT const char* ToString(ax::mojom::ActionFlags action_flags); AX_BASE_EXPORT const char* ToString( ax::mojom::DefaultActionVerb default_action_verb); -// Returns a localized string that corresponds to the name of the given action. -AX_BASE_EXPORT std::string ToLocalizedString( - ax::mojom::DefaultActionVerb action_verb); - // ax::mojom::Mutation AX_BASE_EXPORT const char* ToString(ax::mojom::Mutation mutation); diff --git a/chromium/ui/accessibility/ax_enums.mojom b/chromium/ui/accessibility/ax_enums.mojom index 671d00eaba7..34fcbe230ee 100644 --- a/chromium/ui/accessibility/ax_enums.mojom +++ b/chromium/ui/accessibility/ax_enums.mojom @@ -112,8 +112,7 @@ enum Event { // Next value: 209 [Extensible, Stable, Uuid="d258eb73-e0cc-490c-b881-80ee11d3fec2"] enum Role { - // Used for role="none"/"presentation" -- ignored in platform tree. - [Default]kNone = 0, + [Default]kUnknown = 181, // The role has not been set. kAbbr = 1, kAlert = 2, kAlertDialog = 3, @@ -281,6 +280,7 @@ enum Role { kMenuListPopup = 128, kMeter = 129, kNavigation = 130, + kNone = 0, // Used for role="none"/"presentation"; ignored in platform tree. kNote = 131, kPane = 132, kParagraph = 133, @@ -333,7 +333,6 @@ enum Role { kTree = 178, kTreeGrid = 179, kTreeItem = 180, - kUnknown = 181, kVideo = 182, kWebView = 183, kWindow = 184, @@ -1172,7 +1171,6 @@ enum SortDirection { enum NameFrom { kNone, - kUninitialized, kAttribute, // E.g. aria-label. kAttributeExplicitlyEmpty, kCaption, // E.g. in the case of a table, from a caption element. @@ -1183,17 +1181,55 @@ enum NameFrom { kValue, // E.g. <input type="button" value="Button's name">. }; +// The source of the accessible description. Used by some screen readers +// to determine if and how the description should be presented to the user. enum DescriptionFrom { + // No description has been provided. (See also kAttributeExplicitlyEmpty) kNone, + + // The description comes from a flat string, such as aria-description (in the + // case of web content) or provided by the View. kAriaDescription, - kButtonLabel, // HTML-AAM 5.2.2 + + // The description has been removed to improve accessibility. Example: The + // description normally provided by this View's tooltip contains text which + // is also present in this View's name. This could cause screen readers to + // speak the information twice, which is not desired. Therefore the + // description has been deliberately set to the empty string to prevent + // double presentation. + kAttributeExplicitlyEmpty, + + // The description comes from the label/text of a button. + // See HTML-AAM's Accessible Name and Description Computation. + kButtonLabel, + + // The description comes from some other object such as an element referenced + // by aria-describedby (in the case of web content), or another View present + // in the UI. kRelatedElement, + + // The description comes from a Ruby annotation. kRubyAnnotation, - kSummary, // HTML-AAM 5.8.2 + + // The description comes from the contents of a summary element. + // See HTML-AAM's Accessible Name and Description Computation. + kSummary, + + // The description comes from the text of an SVG desc element. + // See SVG-AAM's Accessible Name and Description Computation. kSvgDescElement, - kTableCaption, // HTML-AAM 5.9.2 + + // The description comes from a table's caption element. + // See HTML-AAM's Accessible Name and Description Computation. + kTableCaption, + + // The description comes from the title attribute (HTML), the title element + // (SVG), or a View's tooltip. kTitle, - kPopupElement, // E.g. |triggerpopup| attr pointing to `popup=hint`. + + // The description comes from a non-tooltip popup, e.g. the |triggerpopup| + // attribute pointing to `popup=hint`. + kPopupElement, }; // Next value: 4 diff --git a/chromium/ui/accessibility/ax_event_generator.cc b/chromium/ui/accessibility/ax_event_generator.cc index e8135a1f6b6..c36911b1552 100644 --- a/chromium/ui/accessibility/ax_event_generator.cc +++ b/chromium/ui/accessibility/ax_event_generator.cc @@ -745,11 +745,6 @@ void AXEventGenerator::OnTreeDataChanged(AXTree* tree, DCHECK_EQ(tree_, tree); DCHECK(tree->root()); - if (new_tree_data.loaded && !old_tree_data.loaded && - ShouldFireLoadEvents(tree->root())) { - AddEvent(tree->root(), Event::LOAD_COMPLETE); - } - if (new_tree_data.title != old_tree_data.title) AddEvent(tree->root(), Event::DOCUMENT_TITLE_CHANGED); @@ -775,7 +770,7 @@ void AXEventGenerator::OnTreeDataChanged(AXTree* tree, // fields, an event should still fire on the field where the selection // ends. if (AXNode* text_field = selection_focus->GetTextFieldAncestor()) - AddEvent(text_field, Event::SELECTION_IN_TEXT_FIELD_CHANGED); + AddEvent(text_field, Event::TEXT_SELECTION_CHANGED); } } } @@ -821,13 +816,6 @@ void AXEventGenerator::OnAtomicUpdateFinished( DCHECK_EQ(tree_, tree); DCHECK(tree->root()); - if (root_changed && ShouldFireLoadEvents(tree->root())) { - if (tree->data().loaded) - AddEvent(tree->root(), Event::LOAD_COMPLETE); - else - AddEvent(tree->root(), Event::LOAD_START); - } - for (const auto& change : changes) { DCHECK(change.node); @@ -974,16 +962,6 @@ void AXEventGenerator::FireRelationSourceEvents(AXTree* tree, }); } -// Attempts to suppress load-related events that we presume no AT will be -// interested in under any circumstances, such as pages which have no size. -bool AXEventGenerator::ShouldFireLoadEvents(AXNode* node) { - if (always_fire_load_complete_) - return true; - - return node->data().relative_bounds.bounds.width() || - node->data().relative_bounds.bounds.height(); -} - void AXEventGenerator::TrimEventsDueToAncestorIgnoredChanged( AXNode* node, std::map<AXNode*, IgnoredChangedStatesBitset>& @@ -1310,10 +1288,6 @@ const char* ToString(AXEventGenerator::Event event) { return "liveRelevantChanged"; case AXEventGenerator::Event::LIVE_STATUS_CHANGED: return "liveStatusChanged"; - case AXEventGenerator::Event::LOAD_COMPLETE: - return "loadComplete"; - case AXEventGenerator::Event::LOAD_START: - return "loadStart"; case AXEventGenerator::Event::MENU_ITEM_SELECTED: return "menuItemSelected"; case ui::AXEventGenerator::Event::MENU_POPUP_END: @@ -1366,8 +1340,8 @@ const char* ToString(AXEventGenerator::Event event) { return "selectedChildrenChanged"; case AXEventGenerator::Event::SELECTED_VALUE_CHANGED: return "selectedValueChanged"; - case AXEventGenerator::Event::SELECTION_IN_TEXT_FIELD_CHANGED: - return "selectionInTextFieldChanged"; + case AXEventGenerator::Event::TEXT_SELECTION_CHANGED: + return "textSelectionChanged"; case AXEventGenerator::Event::SET_SIZE_CHANGED: return "setSizeChanged"; case AXEventGenerator::Event::SORT_CHANGED: diff --git a/chromium/ui/accessibility/ax_event_generator.h b/chromium/ui/accessibility/ax_event_generator.h index 3f3f0a45fdf..a7ec36be769 100644 --- a/chromium/ui/accessibility/ax_event_generator.h +++ b/chromium/ui/accessibility/ax_event_generator.h @@ -86,8 +86,6 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { LIVE_RELEVANT_CHANGED, // Fired only on the root of the ARIA live region. LIVE_STATUS_CHANGED, - LOAD_COMPLETE, - LOAD_START, MENU_ITEM_SELECTED, MENU_POPUP_END, MENU_POPUP_START, @@ -114,12 +112,12 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { SELECTED_CHANGED, SELECTED_CHILDREN_CHANGED, SELECTED_VALUE_CHANGED, - SELECTION_IN_TEXT_FIELD_CHANGED, SET_SIZE_CHANGED, SORT_CHANGED, STATE_CHANGED, SUBTREE_CREATED, TEXT_ATTRIBUTE_CHANGED, + TEXT_SELECTION_CHANGED, VALUE_IN_TEXT_FIELD_CHANGED, // This event is fired for the exact set of attributes that affect the @@ -246,10 +244,6 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { // same order they were added. void AddEvent(ui::AXNode* node, Event event); - void set_always_fire_load_complete(bool val) { - always_fire_load_complete_ = val; - } - void AddEventsForTesting(const AXNode& node, const std::set<EventParams>& events); @@ -329,7 +323,6 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { void FireValueInTextFieldChangedEventIfNecessary(AXTree* tree, AXNode* target_node); void FireRelationSourceEvents(AXTree* tree, AXNode* target_node); - bool ShouldFireLoadEvents(AXNode* node); // Remove excessive events for a tree update containing node. // We remove certain events on a node when it flips its IGNORED state to @@ -364,8 +357,6 @@ class AX_EXPORT AXEventGenerator : public AXTreeObserver { // previously unknown to ATs. std::set<AXNodeID> nodes_to_suppress_parent_changed_on_; - bool always_fire_load_complete_ = false; - // Helper that tracks live regions. std::unique_ptr<AXLiveRegionTracker> live_region_tracker_; diff --git a/chromium/ui/accessibility/ax_event_generator_unittest.cc b/chromium/ui/accessibility/ax_event_generator_unittest.cc index bf9038fb3fb..0af42405df1 100644 --- a/chromium/ui/accessibility/ax_event_generator_unittest.cc +++ b/chromium/ui/accessibility/ax_event_generator_unittest.cc @@ -176,110 +176,6 @@ TEST(AXEventGeneratorTest, IterateThroughEmptyEventSets) { EXPECT_TRUE(expected_event_map.empty()); } -TEST(AXEventGeneratorTest, LoadCompleteSameTree) { - AXTreeUpdate initial_state; - initial_state.root_id = 1; - initial_state.nodes.resize(1); - initial_state.nodes[0].id = 1; - initial_state.nodes[0].relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); - initial_state.has_tree_data = true; - AXTree tree(initial_state); - - AXEventGenerator event_generator(&tree); - ASSERT_THAT(event_generator, IsEmpty()); - AXTreeUpdate load_complete_update = initial_state; - load_complete_update.tree_data.loaded = true; - - ASSERT_TRUE(tree.Unserialize(load_complete_update)); - EXPECT_THAT(event_generator, UnorderedElementsAre(HasEventAtNode( - AXEventGenerator::Event::LOAD_COMPLETE, 1))); -} - -TEST(AXEventGeneratorTest, LoadCompleteNewTree) { - AXTreeUpdate initial_state; - initial_state.root_id = 1; - initial_state.nodes.resize(1); - initial_state.nodes[0].id = 1; - initial_state.has_tree_data = true; - initial_state.tree_data.loaded = true; - AXTree tree(initial_state); - - AXEventGenerator event_generator(&tree); - ASSERT_THAT(event_generator, IsEmpty()); - AXTreeUpdate load_complete_update; - load_complete_update.root_id = 2; - load_complete_update.nodes.resize(1); - load_complete_update.nodes[0].id = 2; - load_complete_update.nodes[0].relative_bounds.bounds = - gfx::RectF(0, 0, 800, 600); - load_complete_update.has_tree_data = true; - load_complete_update.tree_data.loaded = true; - - ASSERT_TRUE(tree.Unserialize(load_complete_update)); - EXPECT_THAT(event_generator, - UnorderedElementsAre( - HasEventAtNode(AXEventGenerator::Event::LOAD_COMPLETE, 2), - HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 2))); - - // Load complete should not be emitted for sizeless roots. - load_complete_update.root_id = 3; - load_complete_update.nodes.resize(1); - load_complete_update.nodes[0].id = 3; - load_complete_update.nodes[0].relative_bounds.bounds = gfx::RectF(0, 0, 0, 0); - load_complete_update.has_tree_data = true; - load_complete_update.tree_data.loaded = true; - - ASSERT_TRUE(tree.Unserialize(load_complete_update)); - EXPECT_THAT(event_generator, - UnorderedElementsAre( - HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 3))); - - // TODO(accessibility): http://crbug.com/888758 - // Load complete should not be emitted for chrome-search URLs. - load_complete_update.root_id = 4; - load_complete_update.nodes.resize(1); - load_complete_update.nodes[0].id = 4; - load_complete_update.nodes[0].relative_bounds.bounds = - gfx::RectF(0, 0, 800, 600); - load_complete_update.nodes[0].AddStringAttribute( - ax::mojom::StringAttribute::kUrl, "chrome-search://foo"); - load_complete_update.has_tree_data = true; - load_complete_update.tree_data.loaded = true; - - ASSERT_TRUE(tree.Unserialize(load_complete_update)); - EXPECT_THAT(event_generator, - UnorderedElementsAre( - HasEventAtNode(AXEventGenerator::Event::LOAD_COMPLETE, 4), - HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 4))); -} - -TEST(AXEventGeneratorTest, LoadStart) { - AXTreeUpdate initial_state; - initial_state.root_id = 1; - initial_state.nodes.resize(1); - initial_state.nodes[0].id = 1; - initial_state.nodes[0].relative_bounds.bounds = gfx::RectF(0, 0, 800, 600); - initial_state.has_tree_data = true; - AXTree tree(initial_state); - - AXEventGenerator event_generator(&tree); - ASSERT_THAT(event_generator, IsEmpty()); - AXTreeUpdate load_start_update; - load_start_update.root_id = 2; - load_start_update.nodes.resize(1); - load_start_update.nodes[0].id = 2; - load_start_update.nodes[0].relative_bounds.bounds = - gfx::RectF(0, 0, 800, 600); - load_start_update.has_tree_data = true; - load_start_update.tree_data.loaded = false; - - ASSERT_TRUE(tree.Unserialize(load_start_update)); - EXPECT_THAT(event_generator, - UnorderedElementsAre( - HasEventAtNode(AXEventGenerator::Event::LOAD_START, 2), - HasEventAtNode(AXEventGenerator::Event::SUBTREE_CREATED, 2))); -} - TEST(AXEventGeneratorTest, DocumentSelectionChanged) { AXTreeUpdate initial_state; initial_state.root_id = 1; @@ -560,15 +456,14 @@ TEST(AXEventGeneratorTest, SelectionInTextFieldChanged) { UnorderedElementsAre( HasEventAtNode(AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED, root.id), - HasEventAtNode( - AXEventGenerator::Event::SELECTION_IN_TEXT_FIELD_CHANGED, - text_field.id))); + HasEventAtNode(AXEventGenerator::Event::TEXT_SELECTION_CHANGED, + text_field.id))); } event_generator.ClearEvents(); { // A selection that does not include a text field in it should not raise the - // "SELECTION_IN_TEXT_FIELD_CHANGED" event. + // "TEXT_SELECTION_CHANGED" event. tree_data.sel_anchor_object_id = root.id; tree_data.sel_anchor_offset = 0; tree_data.sel_focus_object_id = root.id; @@ -587,7 +482,7 @@ TEST(AXEventGeneratorTest, SelectionInTextFieldChanged) { event_generator.ClearEvents(); { // A selection that spans more than one node but which nevertheless ends on - // a text field should still raise the "SELECTION_IN_TEXT_FIELD_CHANGED" + // a text field should still raise the "TEXT_SELECTION_CHANGED" // event. tree_data.sel_anchor_object_id = root.id; tree_data.sel_anchor_offset = 0; @@ -603,9 +498,8 @@ TEST(AXEventGeneratorTest, SelectionInTextFieldChanged) { UnorderedElementsAre( HasEventAtNode(AXEventGenerator::Event::DOCUMENT_SELECTION_CHANGED, root.id), - HasEventAtNode( - AXEventGenerator::Event::SELECTION_IN_TEXT_FIELD_CHANGED, - text_field.id))); + HasEventAtNode(AXEventGenerator::Event::TEXT_SELECTION_CHANGED, + text_field.id))); } } diff --git a/chromium/ui/accessibility/ax_language_detection.h b/chromium/ui/accessibility/ax_language_detection.h index eded0ff39f0..61ec4a9c627 100644 --- a/chromium/ui/accessibility/ax_language_detection.h +++ b/chromium/ui/accessibility/ax_language_detection.h @@ -7,11 +7,11 @@ #include <memory> #include <string> -#include <unordered_map> -#include <unordered_set> #include <utility> #include <vector> +#include "base/containers/flat_map.h" +#include "base/containers/flat_set.h" #include "base/memory/raw_ptr.h" #include "third_party/cld_3/src/src/nnet_language_identifier.h" #include "ui/accessibility/ax_enums.mojom-forward.h" @@ -150,7 +150,7 @@ class AX_EXPORT AXLanguageInfoStats { friend class AXLanguageDetectionTestFixture; // Store a count of the occurrences of a given language. - std::unordered_map<std::string, int> lang_counts_; + base::flat_map<std::string, int> lang_counts_; // Cache of last calculated top language results. // A vector of pairs of (score, language) sorted by descending score. @@ -201,7 +201,7 @@ class AX_EXPORT AXLanguageInfoStats { // Set of top language detected for every node, used to generate the unique // number of detected languages metric (LangsPerPage). - std::unordered_set<std::string> unique_top_lang_detected_; + base::flat_set<std::string> unique_top_lang_detected_; }; // AXLanguageDetectionObserver is registered as a change observer on an AXTree diff --git a/chromium/ui/accessibility/ax_language_detection_unittest.cc b/chromium/ui/accessibility/ax_language_detection_unittest.cc index 6b4bc538128..f7091ae2994 100644 --- a/chromium/ui/accessibility/ax_language_detection_unittest.cc +++ b/chromium/ui/accessibility/ax_language_detection_unittest.cc @@ -10,6 +10,7 @@ #include <memory> #include "base/command_line.h" +#include "base/containers/flat_set.h" #include "base/test/metrics/histogram_tester.h" #include "base/test/scoped_feature_list.h" #include "testing/gtest/include/gtest/gtest.h" @@ -103,7 +104,7 @@ class AXLanguageDetectionTestFixture : public testing::Test { return tree.language_detection_manager->lang_info_stats_.count_overridden_; } - const std::unordered_set<std::string>& unique_top_lang_detected( + const base::flat_set<std::string>& unique_top_lang_detected( AXTree& tree) const { return tree.language_detection_manager->lang_info_stats_ .unique_top_lang_detected_; @@ -672,8 +673,8 @@ TEST_F(AXLanguageDetectionTestStaticContent, MetricCollection) { // There should be 4 unique languages (de, en, fr, es). { const auto& top_lang = unique_top_lang_detected(tree); - const std::unordered_set<std::string> expected_top_lang = {"de", "en", "es", - "fr"}; + const base::flat_set<std::string> expected_top_lang = {"de", "en", "es", + "fr"}; EXPECT_EQ(top_lang, expected_top_lang); } histograms.ExpectUniqueSample("Accessibility.LanguageDetection.LangsPerPage", @@ -1182,7 +1183,7 @@ TEST_F(AXLanguageDetectionTestDynamicContent, MetricCollection) { // There should be 2 unique languages (fr, es). { auto top_lang = unique_top_lang_detected(tree); - const std::unordered_set<std::string> expected_top_lang = {"es", "fr"}; + const base::flat_set<std::string> expected_top_lang = {"es", "fr"}; EXPECT_EQ(top_lang, expected_top_lang); } // There should be a single (unique, 1) value for '2' unique languages. diff --git a/chromium/ui/accessibility/ax_mode.h b/chromium/ui/accessibility/ax_mode.h index 56306c72067..895fa7b9c96 100644 --- a/chromium/ui/accessibility/ax_mode.h +++ b/chromium/ui/accessibility/ax_mode.h @@ -71,7 +71,8 @@ class AX_BASE_EXPORT AXMode { // Update this to include the last supported mode flag. If you add // another, be sure to update the stream insertion operator for - // logging and debugging. + // logging and debugging, as well as AccessibilityModeFlagEnum (and + // related metrics callsites, see: |ModeFlagHistogramValue|). static constexpr uint32_t kLastModeFlag = 1 << 7; constexpr AXMode() : flags_(0) {} @@ -107,6 +108,9 @@ class AX_BASE_EXPORT AXMode { UMA_AX_MODE_INLINE_TEXT_BOXES = 2, UMA_AX_MODE_SCREEN_READER = 3, UMA_AX_MODE_HTML = 4, + UMA_AX_MODE_HTML_METADATA = 5, + UMA_AX_MODE_LABEL_IMAGES = 6, + UMA_AX_MODE_PDF = 7, // This must always be the last enum. It's okay for its value to // increase, but none of the other enum values may change. diff --git a/chromium/ui/accessibility/ax_node.cc b/chromium/ui/accessibility/ax_node.cc index 75687a6e482..452b3907555 100644 --- a/chromium/ui/accessibility/ax_node.cc +++ b/chromium/ui/accessibility/ax_node.cc @@ -4,13 +4,10 @@ #include "ui/accessibility/ax_node.h" -#include <string.h> - #include <algorithm> -#include "base/debug/crash_logging.h" -#include "base/debug/dump_without_crashing.h" #include "base/no_destructor.h" +#include "base/numerics/safe_conversions.h" #include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" @@ -23,7 +20,6 @@ #include "ui/accessibility/ax_table_info.h" #include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_manager.h" -#include "ui/accessibility/ax_tree_manager_map.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/transform.h" @@ -50,7 +46,6 @@ AXNode::AXNode(AXNode::OwnerTree* tree, AXNode::~AXNode() = default; AXNodeData&& AXNode::TakeData() { - has_data_been_taken_ = true; return std::move(data_); } @@ -67,8 +62,7 @@ size_t AXNode::GetChildCount() const { size_t AXNode::GetChildCountCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) return 1u; @@ -85,8 +79,7 @@ size_t AXNode::GetUnignoredChildCountCrossingTreeBoundary() const { // TODO(nektar): Should DCHECK that this node is not ignored. DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) { DCHECK_EQ(unignored_child_count_, 0u) << "A node cannot be hosting both a child tree and other nodes as " @@ -107,8 +100,7 @@ AXNode* AXNode::GetChildAtIndex(size_t index) const { AXNode* AXNode::GetChildAtIndexCrossingTreeBoundary(size_t index) const { DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) { DCHECK_EQ(index, 0u) << "A node cannot be hosting both a child tree and other nodes as " @@ -137,8 +129,7 @@ AXNode* AXNode::GetUnignoredChildAtIndexCrossingTreeBoundary( // TODO(nektar): Should DCHECK that this node is not ignored. DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) { DCHECK_EQ(index, 0u) << "A node cannot be hosting both a child tree and other nodes as " @@ -159,36 +150,17 @@ AXNode* AXNode::GetParentCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (parent_) return parent_; - const AXTreeManager* manager = - AXTreeManagerMap::GetInstance().GetManager(tree_->GetAXTreeID()); + const AXTreeManager* manager = GetManager(); if (manager) return manager->GetParentNodeFromParentTreeAsAXNode(); return nullptr; } AXNode* AXNode::GetUnignoredParent() const { - // TODO(crbug.com/1237353): The following bailout is to test a hypothesis that - // this function is sometimes called while a tree update is in progress or - // when data_ isn't valid, which may be the cause of the crash detailed in - // crbug.com/1237353. Once this hypothesis has been verified, replace the - // bailout with a fix, which ideally should not call this function under - // the circumstances hypothesized. Also, add back in the following line: - // DCHECK(!tree_->GetTreeUpdateInProgressState()); - if (tree_->GetTreeUpdateInProgressState() || !IsDataValid()) { - static auto* const crash_key = base::debug::AllocateCrashKeyString( - "ax_node_err", base::debug::CrashKeySize::Size64); - std::ostringstream error; - error << "dataUninitialized=" << is_data_still_uninitialized_ - << " dataTaken=" << has_data_been_taken_ - << " treeUpdating=" << tree_->GetTreeUpdateInProgressState(); - base::debug::SetCrashKeyString(crash_key, error.str()); - base::debug::DumpWithoutCrashing(); - return nullptr; - } + DCHECK(!tree_->GetTreeUpdateInProgressState()); AXNode* unignored_parent = GetParent(); while (unignored_parent && unignored_parent->IsIgnored()) unignored_parent = unignored_parent->GetParent(); - return unignored_parent; } @@ -196,8 +168,7 @@ AXNode* AXNode::GetUnignoredParentCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); AXNode* unignored_parent = GetUnignoredParent(); if (!unignored_parent) { - const AXTreeManager* manager = - AXTreeManagerMap::GetInstance().GetManager(tree_->GetAXTreeID()); + const AXTreeManager* manager = GetManager(); if (manager) unignored_parent = manager->GetParentNodeFromParentTreeAsAXNode(); } @@ -242,8 +213,7 @@ AXNode* AXNode::GetFirstUnignoredChild() const { AXNode* AXNode::GetFirstUnignoredChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) return child_tree_manager->GetRootAsAXNode(); @@ -274,8 +244,7 @@ AXNode* AXNode::GetLastUnignoredChild() const { AXNode* AXNode::GetLastUnignoredChildCrossingTreeBoundary() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); - const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this); if (child_tree_manager) return child_tree_manager->GetRootAsAXNode(); @@ -288,48 +257,104 @@ AXNode* AXNode::GetDeepestFirstChild() const { return nullptr; AXNode* deepest_child = GetFirstChild(); + DCHECK(deepest_child); while (deepest_child->GetChildCount()) deepest_child = deepest_child->GetFirstChild(); return deepest_child; } +AXNode* AXNode::GetDeepestFirstChildCrossingTreeBoundary() const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + if (!GetChildCountCrossingTreeBoundary()) + return nullptr; + + AXNode* deepest_child = GetFirstChildCrossingTreeBoundary(); + DCHECK(deepest_child); + while (deepest_child->GetChildCountCrossingTreeBoundary()) + deepest_child = deepest_child->GetFirstChildCrossingTreeBoundary(); + + return deepest_child; +} + AXNode* AXNode::GetDeepestFirstUnignoredChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCount()) return nullptr; AXNode* deepest_child = GetFirstUnignoredChild(); + DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCount()) deepest_child = deepest_child->GetFirstUnignoredChild(); return deepest_child; } +AXNode* AXNode::GetDeepestFirstUnignoredChildCrossingTreeBoundary() const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + if (!GetUnignoredChildCountCrossingTreeBoundary()) + return nullptr; + + AXNode* deepest_child = GetFirstUnignoredChildCrossingTreeBoundary(); + DCHECK(deepest_child); + while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary()) + deepest_child = deepest_child->GetFirstUnignoredChildCrossingTreeBoundary(); + + return deepest_child; +} + AXNode* AXNode::GetDeepestLastChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetChildCount()) return nullptr; AXNode* deepest_child = GetLastChild(); + DCHECK(deepest_child); while (deepest_child->GetChildCount()) deepest_child = deepest_child->GetLastChild(); return deepest_child; } +AXNode* AXNode::GetDeepestLastChildCrossingTreeBoundary() const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + if (!GetChildCountCrossingTreeBoundary()) + return nullptr; + + AXNode* deepest_child = GetLastChildCrossingTreeBoundary(); + DCHECK(deepest_child); + while (deepest_child->GetChildCountCrossingTreeBoundary()) + deepest_child = deepest_child->GetLastChildCrossingTreeBoundary(); + + return deepest_child; +} + AXNode* AXNode::GetDeepestLastUnignoredChild() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); if (!GetUnignoredChildCount()) return nullptr; AXNode* deepest_child = GetLastUnignoredChild(); + DCHECK(deepest_child); while (deepest_child->GetUnignoredChildCount()) deepest_child = deepest_child->GetLastUnignoredChild(); return deepest_child; } +AXNode* AXNode::GetDeepestLastUnignoredChildCrossingTreeBoundary() const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + if (!GetUnignoredChildCountCrossingTreeBoundary()) + return nullptr; + + AXNode* deepest_child = GetLastUnignoredChildCrossingTreeBoundary(); + DCHECK(deepest_child); + while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary()) + deepest_child = deepest_child->GetLastUnignoredChildCrossingTreeBoundary(); + + return deepest_child; +} + AXNode* AXNode::GetNextSibling() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); AXNode* parent = GetParent(); @@ -602,6 +627,11 @@ AXNode::UnignoredChildrenCrossingTreeBoundaryEnd() const { return UnignoredChildCrossingTreeBoundaryIterator(this, nullptr); } +bool AXNode::CanFireEvents() const { + // TODO(nektar): Cache the `IsChildOfLeaf` state in `AXComputedNodeData`. + return !IsChildOfLeaf(); +} + absl::optional<int> AXNode::CompareTo(const AXNode& other) const { if (this == &other) return 0; @@ -663,8 +693,6 @@ bool AXNode::IsLineBreak() const { void AXNode::SetData(const AXNodeData& src) { data_ = src; - is_data_still_uninitialized_ = false; - has_data_been_taken_ = false; } void AXNode::SetLocation(AXNodeID offset_container_id, @@ -737,6 +765,78 @@ SkColor AXNode::ComputeColorAttribute(ax::mojom::IntAttribute attr) const { return color; } +AXTreeManager* AXNode::GetManager() const { + return AXTreeManager::FromID(tree_->GetAXTreeID()); +} + +bool AXNode::HasVisibleCaretOrSelection() const { + const OwnerTree::Selection selection = GetSelection(); + const AXNode* focus = tree()->GetFromId(selection.focus_object_id); + if (!focus || !focus->IsDescendantOf(this)) + return false; + + // A selection or the caret will be visible in a focused text field (including + // a content editable). + const AXNode* text_field = GetTextFieldAncestor(); + if (text_field) + return true; + + // The selection will be visible in non-editable content only if it is not + // collapsed. + return !selection.IsCollapsed(); +} + +AXNode::OwnerTree::Selection AXNode::GetSelection() const { + DCHECK(tree()) << "Cannot retrieve the current selection if the node is not " + "attached to an accessibility tree.\n" + << *this; + return tree()->GetSelection(); +} + +AXNode::OwnerTree::Selection AXNode::GetUnignoredSelection() const { + DCHECK(tree()) << "Cannot retrieve the current selection if the node is not " + "attached to an accessibility tree.\n" + << *this; + OwnerTree::Selection selection = tree()->GetUnignoredSelection(); + + // "selection.anchor_offset" and "selection.focus_ofset" might need to be + // adjusted if the anchor or the focus nodes include ignored children. + // + // TODO(nektar): Move this logic into its own "AXSelection" class and cache + // the result for faster reuse. + const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id); + if (anchor && !anchor->IsLeaf()) { + DCHECK_GE(selection.anchor_offset, 0); + if (static_cast<size_t>(selection.anchor_offset) < + anchor->GetChildCount()) { + const AXNode* anchor_child = + anchor->GetChildAtIndex(selection.anchor_offset); + DCHECK(anchor_child); + selection.anchor_offset = + static_cast<int>(anchor_child->GetUnignoredIndexInParent()); + } else { + selection.anchor_offset = + static_cast<int>(anchor->GetUnignoredChildCount()); + } + } + + const AXNode* focus = tree()->GetFromId(selection.focus_object_id); + if (focus && !focus->IsLeaf()) { + DCHECK_GE(selection.focus_offset, 0); + if (static_cast<size_t>(selection.focus_offset) < focus->GetChildCount()) { + const AXNode* focus_child = + focus->GetChildAtIndex(selection.focus_offset); + DCHECK(focus_child); + selection.focus_offset = + static_cast<int>(focus_child->GetUnignoredIndexInParent()); + } else { + selection.focus_offset = + static_cast<int>(focus->GetUnignoredChildCount()); + } + } + return selection; +} + bool AXNode::HasStringAttribute(ax::mojom::StringAttribute attribute) const { return GetComputedNodeData().HasOrCanComputeAttribute(attribute); } @@ -840,7 +940,7 @@ const std::string& AXNode::GetNameUTF8() const { if (GetRole() == ax::mojom::Role::kPortal && GetNameFrom() == ax::mojom::NameFrom::kNone) { const AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManagerForChildTree(*this); + AXTreeManager::ForChildTree(*this); if (child_tree_manager) node = child_tree_manager->GetRootAsAXNode(); } @@ -955,6 +1055,110 @@ int AXNode::GetTextContentLengthUTF16() const { return GetComputedNodeData().GetOrComputeTextContentLengthUTF16(); } +gfx::RectF AXNode::GetTextContentRangeBoundsUTF8(int start_offset, + int end_offset) const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + DCHECK_LE(start_offset, end_offset) + << "Invalid `start_offset` and `end_offset`.\n" + << start_offset << ' ' << end_offset << "\nin\n" + << *this; + // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to + // check whether `start_offset` is also in range. + if (end_offset > GetTextContentLengthUTF8()) + return gfx::RectF(); + + // TODO(nektar): Update this to use + // "base/strings/utf_offset_string_conversions.h" which provides caching of + // offsets. + std::u16string out_trancated_string_utf16; + if (!base::UTF8ToUTF16(GetTextContentUTF8().data(), + base::checked_cast<size_t>(start_offset), + &out_trancated_string_utf16)) { + return gfx::RectF(); + } + start_offset = base::checked_cast<int>(out_trancated_string_utf16.length()); + if (!base::UTF8ToUTF16(GetTextContentUTF8().data(), + base::checked_cast<size_t>(end_offset), + &out_trancated_string_utf16)) { + return gfx::RectF(); + } + end_offset = base::checked_cast<int>(out_trancated_string_utf16.length()); + return GetTextContentRangeBoundsUTF16(start_offset, end_offset); +} + +gfx::RectF AXNode::GetTextContentRangeBoundsUTF16(int start_offset, + int end_offset) const { + DCHECK(!tree_->GetTreeUpdateInProgressState()); + DCHECK_LE(start_offset, end_offset) + << "Invalid `start_offset` and `end_offset`.\n" + << start_offset << ' ' << end_offset << "\nin\n" + << *this; + // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to + // check whether `start_offset` is also in range. + if (end_offset > GetTextContentLengthUTF16()) + return gfx::RectF(); + + const std::vector<int32_t>& character_offsets = + GetIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets); + int character_offsets_length = + base::checked_cast<int>(character_offsets.size()); + // Charactger offsets are always based on the UTF-16 representation of the + // text. + if (character_offsets_length < GetTextContentLengthUTF16()) { + // Blink might not return pixel offsets for all characters. Clamp the + // character range to be within the number of provided pixels. Note that the + // first character always starts at pixel 0, so an offset for that character + // is not provided. + // + // TODO(accessibility): We need to fix this bug in Blink. + start_offset = std::min(start_offset, character_offsets_length); + end_offset = std::min(end_offset, character_offsets_length); + } + + // TODO(nektar): Remove all this code and fix up the character offsets vector + // itself. + int start_pixel_offset = + start_offset > 0 + ? character_offsets[base::checked_cast<size_t>(start_offset - 1)] + : 0; + int end_pixel_offset = + end_offset > 0 + ? character_offsets[base::checked_cast<size_t>(end_offset - 1)] + : 0; + int max_pixel_offset = character_offsets_length > 0 + ? character_offsets[character_offsets_length - 1] + : 0; + const gfx::RectF& node_bounds = data().relative_bounds.bounds; + + gfx::RectF out_bounds; + switch (static_cast<ax::mojom::WritingDirection>( + GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) { + case ax::mojom::WritingDirection::kNone: + case ax::mojom::WritingDirection::kLtr: + out_bounds = gfx::RectF(start_pixel_offset, 0, + end_pixel_offset - start_pixel_offset, + node_bounds.height()); + break; + case ax::mojom::WritingDirection::kRtl: { + int left = max_pixel_offset - end_pixel_offset; + int right = max_pixel_offset - start_pixel_offset; + out_bounds = gfx::RectF(left, 0, right - left, node_bounds.height()); + break; + } + case ax::mojom::WritingDirection::kTtb: + out_bounds = gfx::RectF(0, start_pixel_offset, node_bounds.width(), + end_pixel_offset - start_pixel_offset); + break; + case ax::mojom::WritingDirection::kBtt: { + int top = max_pixel_offset - end_pixel_offset; + int bottom = max_pixel_offset - start_pixel_offset; + out_bounds = gfx::RectF(0, top, node_bounds.width(), bottom - top); + break; + } + } + return out_bounds; +} + std::string AXNode::GetLanguage() const { DCHECK(!tree_->GetTreeUpdateInProgressState()); // Walk up tree considering both detected and author declared languages. @@ -1496,6 +1700,7 @@ bool AXNode::IsIgnoredContainerForOrderedSet() const { GetRole() == ax::mojom::Role::kLabelText || GetRole() == ax::mojom::Role::kListItem || GetRole() == ax::mojom::Role::kGenericContainer || + GetRole() == ax::mojom::Role::kScrollView || GetRole() == ax::mojom::Role::kUnknown; } @@ -1555,12 +1760,21 @@ AXNode* AXNode::GetOrderedSet() const { return result; } -bool AXNode::IsDataValid() const { - return !is_data_still_uninitialized_ && !has_data_been_taken_; -} - bool AXNode::IsReadOnlySupported() const { - return IsCellOrHeaderOfAriaGrid() || ui::IsReadOnlySupported(GetRole()); + // Grid cells and headers can't be derived solely from the role (need to check + // the ancestor chain) so check this first. + if (IsCellOrHeaderOfAriaGrid()) + return true; + + // kPopUpButton is special in that it is the role Blink assigns for both + // role=button with aria-haspopup set, along with <select> elements. + // HTML AAM (https://w3c.github.io/html-aam/) maps <select> to the combobox + // role, which supports readonly, but readonly is not supported for button + // roles. + if (GetRole() == ax::mojom::Role::kPopUpButton && !IsMenuListPopUpButton()) + return false; + + return ui::IsReadOnlySupported(GetRole()); } bool AXNode::IsReadOnlyOrDisabled() const { @@ -1675,6 +1889,7 @@ bool AXNode::IsInvisibleOrIgnored() const { } bool AXNode::IsChildOfLeaf() const { + // TODO(nektar): Cache this state in `AXComputedNodeData`. for (const AXNode* ancestor = GetUnignoredParent(); ancestor; ancestor = ancestor->GetUnignoredParent()) { if (ancestor->IsLeaf()) @@ -1829,11 +2044,9 @@ bool AXNode::IsInListMarker() const { grandparent_node->GetRole() == ax::mojom::Role::kListMarker; } -bool AXNode::IsCollapsedMenuListPopUpButton() const { - if (GetRole() != ax::mojom::Role::kPopUpButton || - !HasState(ax::mojom::State::kCollapsed)) { +bool AXNode::IsMenuListPopUpButton() const { + if (GetRole() != ax::mojom::Role::kPopUpButton) return false; - } // When a popup button contains a menu list popup, its only child is unignored // and is a menu list popup. @@ -1844,6 +2057,22 @@ bool AXNode::IsCollapsedMenuListPopUpButton() const { return node->GetRole() == ax::mojom::Role::kMenuListPopup; } +bool AXNode::IsCollapsedMenuListPopUpButton() const { + if (!HasState(ax::mojom::State::kCollapsed)) + return false; + + return IsMenuListPopUpButton(); +} + +bool AXNode::IsRootWebAreaForPresentationalIframe() const { + if (!ui::IsPlatformDocument(GetRole())) + return false; + const AXNode* parent = GetUnignoredParentCrossingTreeBoundary(); + if (!parent) + return false; + return parent->GetRole() == ax::mojom::Role::kIframePresentational; +} + AXNode* AXNode::GetCollapsedMenuListPopUpButtonAncestor() const { AXNode* node = GetOrderedSet(); @@ -1911,6 +2140,51 @@ AXNode* AXNode::GetTextFieldAncestor() const { return nullptr; } +AXNode* AXNode::GetTextFieldInnerEditorElement() const { + if (!data().IsAtomicTextField() || !GetUnignoredChildCount()) + return nullptr; + + // Text fields wrap their static text and inline text boxes in generic + // containers, and some, like <input type="search">, wrap the wrapper as well. + // There are several incarnations of this structure. + // 1. An empty atomic text field: + // -- Generic container <-- there can be any number of these in a chain. + // However, some empty text fields have the below structure, with empty + // text boxes. + // 2. A single line, an atomic text field with some text in it: + // -- Generic container <-- there can be any number of these in a chain. + // ---- Static text + // ------ Inline text box children (zero or more) + // ---- Line Break (optional, a placeholder break element if the text data + // ends with '\n' or '\r') + // 3. A multiline textarea with some text in it: + // Similar to #2, but can repeat the static text, line break children + // multiple times. + + AXNode* text_container = GetDeepestFirstUnignoredChild(); + DCHECK(text_container) << "Unable to retrieve deepest unignored child on\n" + << *this; + // Non-empty text fields expose a set of static text objects with one or more + // inline text boxes each. On some platforms, such as Android, we don't enable + // inline text boxes, and only the static text objects are exposed. + if (text_container->GetRole() == ax::mojom::Role::kInlineTextBox) + text_container = text_container->GetUnignoredParent(); + + // Get the parent of the static text or the line break, if any; a line break + // is possible when the field contains a line break as its first character. + if (text_container->GetRole() == ax::mojom::Role::kStaticText || + text_container->GetRole() == ax::mojom::Role::kLineBreak) { + text_container = text_container->GetUnignoredParent(); + } + + DCHECK(text_container) << "Unexpected unignored parent while computing text " + "field inner editor element on\n" + << *this; + if (text_container->GetRole() == ax::mojom::Role::kGenericContainer) + return text_container; + return nullptr; +} + AXNode* AXNode::GetSelectionContainer() const { for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor; ancestor = ancestor->GetUnignoredParent()) { diff --git a/chromium/ui/accessibility/ax_node.h b/chromium/ui/accessibility/ax_node.h index 2a5efdc2176..2a98042c335 100644 --- a/chromium/ui/accessibility/ax_node.h +++ b/chromium/ui/accessibility/ax_node.h @@ -24,11 +24,14 @@ #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_text_attributes.h" #include "ui/accessibility/ax_tree_id.h" +#include "ui/gfx/geometry/rect_f.h" namespace ui { class AXComputedNodeData; class AXTableInfo; +class AXTreeManager; + struct AXLanguageInfo; struct AXTreeData; @@ -58,13 +61,27 @@ class AX_EXPORT AXNode final { // be necessary. class OwnerTree { public: - struct Selection { - bool is_backward; - AXNodeID anchor_object_id; - int anchor_offset; + // A data structure that can store either the selected range of nodes in the + // accessibility tree, or the location of the caret in the case of a + // "collapsed" selection. + // + // TODO(nektar): Move this struct into its own file called "AXSelection", + // turn it into a class and make it compute the unignored selection given + // the `AXTreeData`. + struct Selection final { + // Returns true if this instance represents the position of the caret. + constexpr bool IsCollapsed() const { + return focus_object_id != kInvalidAXNodeID && + anchor_object_id == focus_object_id && + anchor_offset == focus_offset; + } + + bool is_backward = false; + AXNodeID anchor_object_id = kInvalidAXNodeID; + int anchor_offset = -1; ax::mojom::TextAffinity anchor_affinity; - AXNodeID focus_object_id; - int focus_offset; + AXNodeID focus_object_id = kInvalidAXNodeID; + int focus_offset = -1; ax::mojom::TextAffinity focus_affinity; }; @@ -80,8 +97,13 @@ class AX_EXPORT AXNode final { virtual absl::optional<int> GetPosInSet(const AXNode& node) = 0; virtual absl::optional<int> GetSetSize(const AXNode& node) = 0; + // See `AXTree::GetSelection`. + virtual Selection GetSelection() const = 0; + // See `AXTree::GetUnignoredSelection`. virtual Selection GetUnignoredSelection() const = 0; + // See `AXTree::GetTreeUpdateInProgressState`. virtual bool GetTreeUpdateInProgressState() const = 0; + // See `AXTree::HasPaginationSupport`. virtual bool HasPaginationSupport() const = 0; }; @@ -111,7 +133,7 @@ class AX_EXPORT AXNode final { protected: raw_ptr<const NodeType> parent_; - raw_ptr<NodeType> child_; + raw_ptr<NodeType, DanglingUntriaged> child_; }; // The constructor requires a parent, id, and index in parent, but @@ -122,7 +144,7 @@ class AX_EXPORT AXNode final { AXNode* parent, AXNodeID id, size_t index_in_parent, - size_t unignored_index_in_parent = 0); + size_t unignored_index_in_parent = 0u); virtual ~AXNode(); // Accessors. @@ -167,10 +189,18 @@ class AX_EXPORT AXNode final { AXNode* GetLastChildCrossingTreeBoundary() const; AXNode* GetLastUnignoredChild() const; AXNode* GetLastUnignoredChildCrossingTreeBoundary() const; + + // TODO(accessibility): Consider renaming all "GetDeepest...Child" methods to + // "GetDeepest...Descendant". AXNode* GetDeepestFirstChild() const; + AXNode* GetDeepestFirstChildCrossingTreeBoundary() const; AXNode* GetDeepestFirstUnignoredChild() const; + AXNode* GetDeepestFirstUnignoredChildCrossingTreeBoundary() const; AXNode* GetDeepestLastChild() const; + AXNode* GetDeepestLastChildCrossingTreeBoundary() const; AXNode* GetDeepestLastUnignoredChild() const; + AXNode* GetDeepestLastUnignoredChildCrossingTreeBoundary() const; + AXNode* GetNextSibling() const; AXNode* GetNextUnignoredSibling() const; AXNode* GetPreviousSibling() const; @@ -231,6 +261,12 @@ class AX_EXPORT AXNode final { UnignoredChildCrossingTreeBoundaryIterator UnignoredChildrenCrossingTreeBoundaryEnd() const; + // Returns true if this is a node on which accessibility events make sense to + // be fired. Events are not needed on nodes that will, for example, never + // appear in a tree that is visible to assistive software, as there will be no + // software to handle the event on the other end. + bool CanFireEvents() const; + // Returns an optional integer indicating the logical order of this node // compared to another node, or returns an empty optional if the nodes are not // comparable. Nodes are not comparable if they do not share a common @@ -246,6 +282,8 @@ class AX_EXPORT AXNode final { // be before (logically less) the node we visit later. absl::optional<int> CompareTo(const AXNode& other) const; + bool IsDataValid() const { return data_.id != kInvalidAXNodeID; } + // Returns true if the node has any of the text related roles, including // kStaticText, kInlineTextBox and kListMarker (for Legacy Layout). Does not // include any text field roles. @@ -292,6 +330,24 @@ class AX_EXPORT AXNode final { SkColor ComputeColor() const; SkColor ComputeBackgroundColor() const; + AXTreeManager* GetManager() const; + + // + // Methods for accessing caret and selection information. + // + + // Returns true if the caret is visible or there is an active selection inside + // this node. + bool HasVisibleCaretOrSelection() const; + + // Gets the current selection from the accessibility tree. + OwnerTree::Selection GetSelection() const; + + // Gets the unignored selection from the accessibility tree, meaning the + // selection whose endpoints are on unignored nodes. (An "ignored" node is a + // node that is not exposed to platform APIs: See `IsIgnored`.) + OwnerTree::Selection GetUnignoredSelection() const; + // // Methods for accessing accessibility attributes including attributes that // are computed on the browser side. (See `AXNodeData` and @@ -388,6 +444,9 @@ class AX_EXPORT AXNode final { bool HasHtmlAttribute(const char* attribute) const { return data().HasHtmlAttribute(attribute); } + std::u16string GetHtmlAttribute(const char* attribute) const { + return data().GetHtmlAttribute(attribute); + } bool GetHtmlAttribute(const char* attribute, std::string* value) const { return data().GetHtmlAttribute(attribute, value); } @@ -492,6 +551,18 @@ class AX_EXPORT AXNode final { int GetTextContentLengthUTF8() const; int GetTextContentLengthUTF16() const; + // Returns the smallest bounding box that can enclose the given range of + // characters in the node's text contents. The bounding box is relative to + // this node's coordinate system as specified in + // `AXNodeData::relative_bounds`. + // + // Note that `start_offset` and `end_offset` are either in UTF8 or UTF16 code + // units, not in grapheme clusters. + gfx::RectF GetTextContentRangeBoundsUTF8(int start_offset, + int end_offset) const; + gfx::RectF GetTextContentRangeBoundsUTF16(int start_offset, + int end_offset) const; + // Returns a string representing the language code. // // This will consider the language declared in the DOM, and may eventually @@ -659,10 +730,18 @@ class AX_EXPORT AXNode final { // of a list marker node. Returns false otherwise. bool IsInListMarker() const; + // Returns true if this node is a popup button that is a parent to a menu list + // popup. + bool IsMenuListPopUpButton() const; + // Returns true if this node is a collapsed popup button that is parent to a // menu list popup. bool IsCollapsedMenuListPopUpButton() const; + // Returns true if this node is at the root of an accessibility tree that is + // hosted by a presentational iframe. + bool IsRootWebAreaForPresentationalIframe() const; + // Returns the popup button ancestor of this current node if any. The popup // button needs to be the parent of a menu list popup and needs to be // collapsed. @@ -680,6 +759,11 @@ class AX_EXPORT AXNode final { // contenteditable without the role, (see `AXNodeData::IsTextField()`). AXNode* GetTextFieldAncestor() const; + // Get the native text field's deepest container; the lowest descendant that + // contains all its text. Returns nullptr if the text field is empty, or if it + // is not an atomic text field, (e.g., <input> or <textarea>). + AXNode* GetTextFieldInnerEditorElement() const; + // If this node is within a container (or widget) that supports either single // or multiple selection, returns the node that represents the container. AXNode* GetSelectionContainer() const; @@ -698,10 +782,6 @@ class AX_EXPORT AXNode final { // Finds and returns a pointer to ordered set containing node. AXNode* GetOrderedSet() const; - // Returns false if the |data_| is uninitialized or has been taken. Returns - // true otherwise. - bool IsDataValid() const; - // Returns true if the node supports the read-only attribute. bool IsReadOnlySupported() const; @@ -741,13 +821,6 @@ class AX_EXPORT AXNode final { // computed by the tree's source, such as `content::BlinkAXTreeSource`. AXNodeData data_; - // Used to track when this object's data_ is valid. If either of these are - // true, and data is accessed, there will be a crash. - // TODO(crbug.com/1237353): Wrap this inside of an `#if DCHECK_IS_ON()` after - // removing `DumpWithoutCrashing`. - bool is_data_still_uninitialized_ = true; - bool has_data_been_taken_ = false; - // See the class comment in "ax_hypertext.h" for an explanation of this // member. mutable AXHypertext hypertext_; diff --git a/chromium/ui/accessibility/ax_node_data.cc b/chromium/ui/accessibility/ax_node_data.cc index 639485b34ec..6179db7ebcf 100644 --- a/chromium/ui/accessibility/ax_node_data.cc +++ b/chromium/ui/accessibility/ax_node_data.cc @@ -257,9 +257,14 @@ AXNodeData::AXNodeData(AXNodeData&& other) { html_attributes.swap(other.html_attributes); child_ids.swap(other.child_ids); relative_bounds = other.relative_bounds; + + other.id = kInvalidAXNodeID; + other.role = ax::mojom::Role::kUnknown; + other.state = 0U; + other.actions = 0ULL; } -AXNodeData& AXNodeData::operator=(AXNodeData other) { +AXNodeData& AXNodeData::operator=(const AXNodeData& other) { id = other.id; role = other.role; state = other.state; @@ -458,6 +463,12 @@ bool AXNodeData::GetHtmlAttribute(const char* attribute, return false; } +std::u16string AXNodeData::GetHtmlAttribute(const char* attribute) const { + std::u16string value_utf16; + GetHtmlAttribute(attribute, &value_utf16); + return value_utf16; +} + bool AXNodeData::GetHtmlAttribute(const char* attribute, std::u16string* value) const { std::string value_utf8; @@ -607,9 +618,15 @@ AXTextAttributes AXNodeData::GetTextAttributes() const { } void AXNodeData::SetName(const std::string& name) { - DCHECK_NE(role, ax::mojom::Role::kNone) - << "A valid role is required before setting the name attribute, because " - "the role is used for setting the required NameFrom attribute."; + // Elements with role='presentation' have Role::kNone. They should not be + // named. This check is only relevant if the name is not empty. + // TODO(accessibility): It would be nice to have a means to set the name + // and role at the same time to avoid this ordering requirement. + DCHECK(role != ax::mojom::Role::kNone || name.empty()) + << "Cannot set name to '" << name << "' on class: '" + << GetStringAttribute(ax::mojom::StringAttribute::kClassName) + << "' because a valid role is needed to set the default NameFrom " + "attribute. Set the role first."; auto iter = std::find_if(string_attributes.begin(), string_attributes.end(), [](const auto& string_attribute) { @@ -623,6 +640,13 @@ void AXNodeData::SetName(const std::string& name) { iter->second = name; } + // It is possible for SetName to be called after + // SetNameExplicitlyEmpty. + if (!name.empty() && + GetNameFrom() == ax::mojom::NameFrom::kAttributeExplicitlyEmpty) { + RemoveIntAttribute(ax::mojom::IntAttribute::kNameFrom); + } + if (HasIntAttribute(ax::mojom::IntAttribute::kNameFrom)) return; // Since this method is mostly used by tests which don't always set the @@ -650,6 +674,7 @@ void AXNodeData::SetName(const std::u16string& name) { void AXNodeData::SetNameExplicitlyEmpty() { SetNameFrom(ax::mojom::NameFrom::kAttributeExplicitlyEmpty); + SetName(std::string()); } void AXNodeData::SetDescription(const std::string& description) { diff --git a/chromium/ui/accessibility/ax_node_data.h b/chromium/ui/accessibility/ax_node_data.h index 9d2c3e605d9..6504ead5672 100644 --- a/chromium/ui/accessibility/ax_node_data.h +++ b/chromium/ui/accessibility/ax_node_data.h @@ -49,7 +49,7 @@ struct AX_BASE_EXPORT AXNodeData { AXNodeData(const AXNodeData& other); AXNodeData(AXNodeData&& other); - AXNodeData& operator=(AXNodeData other); + AXNodeData& operator=(const AXNodeData& other); // Accessing accessibility attributes: // @@ -104,6 +104,7 @@ struct AX_BASE_EXPORT AXNodeData { bool HasHtmlAttribute(const char* attribute) const; bool GetHtmlAttribute(const char* attribute, std::string* value) const; + std::u16string GetHtmlAttribute(const char* attribute) const; bool GetHtmlAttribute(const char* attribute, std::u16string* value) const; // diff --git a/chromium/ui/accessibility/ax_node_data_unittest.cc b/chromium/ui/accessibility/ax_node_data_unittest.cc index a9bfa61272e..2092adb21cf 100644 --- a/chromium/ui/accessibility/ax_node_data_unittest.cc +++ b/chromium/ui/accessibility/ax_node_data_unittest.cc @@ -10,6 +10,7 @@ #include <utility> #include "base/containers/contains.h" +#include "base/test/gtest_util.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_enums.mojom.h" @@ -363,6 +364,43 @@ TEST(AXNodeDataTest, SupportsExpandCollapse) { } } +TEST(AXNodeDataTest, SetName) { + AXNodeData data; + // SetName should not be called on a role of kNone. That role is used for + // presentational objects which should not be included in the accessibility + // tree. This is enforced by a DCHECK. + data.role = ax::mojom::Role::kNone; + EXPECT_DCHECK_DEATH(data.SetName("role is presentational")); + + // For roles other than text, setting the name should result in the NameFrom + // source being kAttribute. + data.role = ax::mojom::Role::kButton; + data.SetName("foo"); + EXPECT_EQ("foo", data.GetStringAttribute(ax::mojom::StringAttribute::kName)); + EXPECT_EQ(data.GetNameFrom(), ax::mojom::NameFrom::kAttribute); + + // TODO(accessibility): The static text role should have a NameFrom source of + // kContents. But nothing clears the NameFrom if the role of an existing + // object changes because currently there is no AXNodeData::SetRole method. + data.role = ax::mojom::Role::kStaticText; + data.SetName("bar"); + EXPECT_EQ("bar", data.GetStringAttribute(ax::mojom::StringAttribute::kName)); + EXPECT_EQ(data.GetNameFrom(), ax::mojom::NameFrom::kAttribute); + + data.RemoveIntAttribute(ax::mojom::IntAttribute::kNameFrom); + data.SetName("baz"); + EXPECT_EQ("baz", data.GetStringAttribute(ax::mojom::StringAttribute::kName)); + EXPECT_EQ(data.GetNameFrom(), ax::mojom::NameFrom::kContents); + + data.SetNameExplicitlyEmpty(); + EXPECT_EQ("", data.GetStringAttribute(ax::mojom::StringAttribute::kName)); + EXPECT_EQ(data.GetNameFrom(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty); + + data.SetName("foo"); + EXPECT_EQ("foo", data.GetStringAttribute(ax::mojom::StringAttribute::kName)); + EXPECT_EQ(data.GetNameFrom(), ax::mojom::NameFrom::kContents); +} + TEST(AXNodeDataTest, BitFieldsSanityCheck) { EXPECT_LT(static_cast<size_t>(ax::mojom::State::kMaxValue), sizeof(AXNodeData::state) * 8); diff --git a/chromium/ui/accessibility/ax_node_position.cc b/chromium/ui/accessibility/ax_node_position.cc index 6af347c4e02..6f41e0724de 100644 --- a/chromium/ui/accessibility/ax_node_position.cc +++ b/chromium/ui/accessibility/ax_node_position.cc @@ -22,8 +22,6 @@ AXEmbeddedObjectBehavior g_ax_embedded_object_behavior = AXEmbeddedObjectBehavior::kSuppressCharacter; #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(USE_ATK) -namespace testing { - ScopedAXEmbeddedObjectBehaviorSetter::ScopedAXEmbeddedObjectBehaviorSetter( AXEmbeddedObjectBehavior behavior) { prev_behavior_ = g_ax_embedded_object_behavior; @@ -34,8 +32,6 @@ ScopedAXEmbeddedObjectBehaviorSetter::~ScopedAXEmbeddedObjectBehaviorSetter() { g_ax_embedded_object_behavior = prev_behavior_; } -} // namespace testing - std::string ToString(const AXPositionKind kind) { static constexpr auto kKindToString = base::MakeFixedFlatMap<AXPositionKind, const char*>( diff --git a/chromium/ui/accessibility/ax_node_position_fuzzer.cc b/chromium/ui/accessibility/ax_node_position_fuzzer.cc new file mode 100644 index 00000000000..6c58f8254a6 --- /dev/null +++ b/chromium/ui/accessibility/ax_node_position_fuzzer.cc @@ -0,0 +1,410 @@ +// Copyright 2022 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 "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_node_data.h" +#include "ui/accessibility/ax_node_position.h" +#include "ui/accessibility/ax_position.h" +#include "ui/accessibility/ax_range.h" +#include "ui/accessibility/ax_role_properties.h" +#include "ui/accessibility/ax_tree.h" +#include "ui/accessibility/ax_tree_data.h" +#include "ui/accessibility/ax_tree_fuzzer_util.h" +#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree_update.h" +#include "ui/accessibility/test_ax_tree_manager.h" + +// Max amount of fuzz data needed to create the next position +const size_t kNextNodePositionMaxDataSize = 4; + +// Min/Max node size for generated tree. +const size_t kMinNodeCount = 10; +const size_t kMaxNodeCount = kMinNodeCount + 50; + +// Min fuzz data needed for fuzzer to function. +// Tree of minimum size with text for each node + 2 positions. +const size_t kMinFuzzDataSize = + kMinNodeCount * AXTreeFuzzerGenerator::kMinimumNewNodeFuzzDataSize + + kMinNodeCount * AXTreeFuzzerGenerator::kMinTextFuzzDataSize + + 2 * kNextNodePositionMaxDataSize; +// Cap fuzz data to avoid slowness. +const size_t kMaxFuzzDataSize = 20000; + +using TestPositionType = + std::unique_ptr<ui::AXPosition<ui::AXNodePosition, ui::AXNode>>; +using TestPositionRange = + ui::AXRange<ui::AXPosition<ui::AXNodePosition, ui::AXNode>>; + +// Helper to create positions in the given tree. +class AXNodePositionFuzzerGenerator { + public: + AXNodePositionFuzzerGenerator(ui::AXTree* tree, + ui::AXNodeID max_id, + FuzzerData& fuzzer_data); + + TestPositionType CreateNewPosition(); + TestPositionType GenerateNextPosition(TestPositionType& current_position, + TestPositionType& previous_position); + + static void CallPositionAPIs(TestPositionType& position, + TestPositionType& other_position); + + private: + static ax::mojom::MoveDirection GenerateMoveDirection(unsigned char byte); + static ax::mojom::TextAffinity GenerateTextAffinity(unsigned char byte); + static ui::AXPositionKind GeneratePositionKind(unsigned char byte); + static ui::AXPositionAdjustmentBehavior GenerateAdjustmentBehavior( + unsigned char byte); + static ui::AXMovementOptions GenerateMovementOptions( + unsigned char behavior_byte, + unsigned char detection_byte); + + TestPositionType CreateNewPosition(ui::AXNodeID anchor_id, + int child_index_or_text_offset, + ui::AXPositionKind position_kind, + ax::mojom::TextAffinity affinity); + + ui::AXTree* tree_; + const ui::AXNodeID max_id_; + FuzzerData& fuzzer_data_; +}; + +AXNodePositionFuzzerGenerator::AXNodePositionFuzzerGenerator( + ui::AXTree* tree, + ui::AXNodeID max_id, + FuzzerData& fuzzer_data) + : tree_(tree), max_id_(max_id), fuzzer_data_(fuzzer_data) {} + +TestPositionType AXNodePositionFuzzerGenerator::CreateNewPosition() { + return CreateNewPosition(fuzzer_data_.NextByte(), fuzzer_data_.NextByte(), + GeneratePositionKind(fuzzer_data_.NextByte()), + GenerateTextAffinity(fuzzer_data_.NextByte())); +} + +TestPositionType AXNodePositionFuzzerGenerator::CreateNewPosition( + ui::AXNodeID anchor_id, + int child_index_or_text_offset, + ui::AXPositionKind position_kind, + ax::mojom::TextAffinity affinity) { + // To ensure that anchor_id is between |ui::kInvalidAXNodeID| and the max ID + // of the tree (non-inclusive), get a number [0, max_id - 1) and then shift by + // 1 to get [1, max_id) + anchor_id = (anchor_id % (max_id_ - 1)) + 1; + ui::AXNode* anchor = tree_->GetFromId(anchor_id); + DCHECK(anchor); + + switch (position_kind) { + case ui::AXPositionKind::TREE_POSITION: + // Avoid division by zero in the case where the node has no children. + child_index_or_text_offset = + anchor->GetChildCount() + ? child_index_or_text_offset % anchor->GetChildCount() + : 0; + return ui::AXNodePosition::CreateTreePosition( + tree_->GetAXTreeID(), anchor_id, child_index_or_text_offset); + case ui::AXPositionKind::TEXT_POSITION: { + // Avoid division by zero in the case where the node has no text. + child_index_or_text_offset = + anchor->GetTextContentLengthUTF16() + ? child_index_or_text_offset % anchor->GetTextContentLengthUTF16() + : 0; + return ui::AXNodePosition::CreateTextPosition( + tree_->GetAXTreeID(), anchor_id, child_index_or_text_offset, + affinity); + case ui::AXPositionKind::NULL_POSITION: + NOTREACHED(); + return ui::AXNodePosition::CreateNullPosition(); + } + } +} + +ax::mojom::MoveDirection AXNodePositionFuzzerGenerator::GenerateMoveDirection( + unsigned char byte) { + constexpr unsigned char max_value = + static_cast<unsigned char>(ax::mojom::MoveDirection::kMaxValue); + return static_cast<ax::mojom::MoveDirection>(byte % max_value); +} + +ax::mojom::TextAffinity AXNodePositionFuzzerGenerator::GenerateTextAffinity( + unsigned char byte) { + constexpr unsigned char max_value = + static_cast<unsigned char>(ax::mojom::TextAffinity::kMaxValue); + return static_cast<ax::mojom::TextAffinity>(byte % max_value); +} + +ui::AXPositionKind AXNodePositionFuzzerGenerator::GeneratePositionKind( + unsigned char byte) { + return byte % 2 ? ui::AXPositionKind::TREE_POSITION + : ui::AXPositionKind::TEXT_POSITION; +} + +ui::AXPositionAdjustmentBehavior +AXNodePositionFuzzerGenerator::GenerateAdjustmentBehavior(unsigned char byte) { + return byte % 2 ? ui::AXPositionAdjustmentBehavior::kMoveBackward + : ui::AXPositionAdjustmentBehavior::kMoveForward; +} + +ui::AXMovementOptions AXNodePositionFuzzerGenerator::GenerateMovementOptions( + unsigned char behavior_byte, + unsigned char detection_byte) { + return ui::AXMovementOptions( + static_cast<ui::AXBoundaryBehavior>(behavior_byte % 3), + static_cast<ui::AXBoundaryDetection>(detection_byte % 3)); +} + +TestPositionType AXNodePositionFuzzerGenerator::GenerateNextPosition( + TestPositionType& current_position, + TestPositionType& previous_position) { + switch (fuzzer_data_.NextByte() % 55) { + case 0: + default: + return CreateNewPosition(); + case 1: + return current_position->AsValidPosition(); + case 2: + return current_position->AsTreePosition(); + case 3: + return current_position->AsLeafTreePosition(); + case 4: + return current_position->AsTextPosition(); + case 5: + return current_position->AsLeafTextPosition(); + case 6: + return current_position->AsDomSelectionPosition(); + case 7: + return current_position->AsUnignoredPosition( + GenerateAdjustmentBehavior(fuzzer_data_.NextByte())); + case 8: + return current_position->CreateAncestorPosition( + previous_position->GetAnchor(), + GenerateMoveDirection(fuzzer_data_.NextByte())); + case 9: + return current_position->CreatePositionAtStartOfAnchor(); + case 10: + return current_position->CreatePositionAtEndOfAnchor(); + case 11: + return current_position->CreatePositionAtStartOfAXTree(); + case 12: + return current_position->CreatePositionAtEndOfAXTree(); + case 13: + return current_position->CreatePositionAtStartOfContent(); + case 14: + return current_position->CreatePositionAtEndOfContent(); + case 15: + return current_position->CreateChildPositionAt(fuzzer_data_.NextByte() % + 10); + case 16: + return current_position->CreateParentPosition( + GenerateMoveDirection(fuzzer_data_.NextByte())); + case 17: + return current_position->CreateNextLeafTreePosition(); + case 18: + return current_position->CreatePreviousLeafTreePosition(); + case 19: + return current_position->CreateNextLeafTextPosition(); + case 20: + return current_position->CreatePreviousLeafTextPosition(); + case 21: + return current_position->AsLeafTextPositionBeforeCharacter(); + case 22: + return current_position->AsLeafTextPositionAfterCharacter(); + case 23: + return current_position->CreatePreviousCharacterPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 24: + return current_position->CreateNextWordStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 25: + return current_position->CreatePreviousWordStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 26: + return current_position->CreateNextWordEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 27: + return current_position->CreatePreviousWordEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 28: + return current_position->CreateNextLineStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 29: + return current_position->CreatePreviousLineStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 30: + return current_position->CreateNextLineEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 31: + return current_position->CreatePreviousLineEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 32: + return current_position->CreateNextFormatStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 33: + return current_position->CreatePreviousFormatStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 34: + return current_position->CreateNextFormatEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 35: + return current_position->CreatePreviousFormatEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 36: + return current_position->CreateNextSentenceStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 37: + return current_position->CreatePreviousSentenceStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 38: + return current_position->CreateNextSentenceEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 39: + return current_position->CreatePreviousSentenceEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 40: + return current_position->CreateNextParagraphStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 41: + return current_position + ->CreateNextParagraphStartPositionSkippingEmptyParagraphs( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 42: + return current_position->CreatePreviousParagraphStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 43: + return current_position + ->CreatePreviousParagraphStartPositionSkippingEmptyParagraphs( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 44: + return current_position->CreateNextParagraphEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 45: + return current_position->CreatePreviousParagraphEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 46: + return current_position->CreateNextPageStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 47: + return current_position->CreatePreviousPageStartPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 48: + return current_position->CreateNextPageEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 49: + return current_position->CreatePreviousPageEndPosition( + GenerateMovementOptions(fuzzer_data_.NextByte(), + fuzzer_data_.NextByte())); + case 52: + return current_position->CreateNextAnchorPosition(); + case 53: + return current_position->CreatePreviousAnchorPosition(); + case 54: + return current_position->LowestCommonAncestorPosition( + *previous_position, GenerateMoveDirection(fuzzer_data_.NextByte())); + } +} + +void AXNodePositionFuzzerGenerator::CallPositionAPIs( + TestPositionType& position, + TestPositionType& other_position) { + // Call APIs on the created position. We don't care about any of the results, + // we just want to make sure none of these crash or hang. + std::ignore = position->GetAnchor(); + std::ignore = position->GetAnchorSiblingCount(); + std::ignore = position->IsIgnored(); + std::ignore = position->IsLeaf(); + std::ignore = position->IsValid(); + std::ignore = position->AtStartOfWord(); + std::ignore = position->AtEndOfWord(); + std::ignore = position->AtStartOfLine(); + std::ignore = position->AtEndOfLine(); + std::ignore = position->GetFormatStartBoundaryType(); + std::ignore = position->GetFormatEndBoundaryType(); + std::ignore = position->AtStartOfSentence(); + std::ignore = position->AtEndOfSentence(); + std::ignore = position->AtStartOfParagraph(); + std::ignore = position->AtEndOfParagraph(); + std::ignore = position->AtStartOfInlineBlock(); + std::ignore = position->AtStartOfPage(); + std::ignore = position->AtEndOfPage(); + std::ignore = position->AtStartOfAXTree(); + std::ignore = position->AtEndOfAXTree(); + std::ignore = position->AtStartOfContent(); + std::ignore = position->AtEndOfContent(); + std::ignore = position->LowestCommonAnchor(*other_position); + std::ignore = position->CompareTo(*other_position); + std::ignore = position->GetText(); + std::ignore = position->IsPointingToLineBreak(); + std::ignore = position->IsInTextObject(); + std::ignore = position->IsInWhiteSpace(); + std::ignore = position->MaxTextOffset(); + std::ignore = position->GetRole(); +} + +// Entry point for LibFuzzer. +extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data, size_t size) { + if (size < kMinFuzzDataSize || size > kMaxFuzzDataSize) + return 0; + AXTreeFuzzerGenerator generator; + FuzzerData fuzz_data(data, size); + const size_t node_count = + kMinNodeCount + fuzz_data.NextByte() % kMaxNodeCount; + generator.GenerateInitialUpdate(fuzz_data, node_count); + ui::AXNodeID max_id = generator.GetMaxAssignedID(); + + ui::AXTree* tree = generator.GetTree(); + + // Run with --v=1 to aid in debugging a specific crash. + VLOG(1) << tree->ToString(); + + // Check to ensure there is enough fuzz data to create two positions. + if (fuzz_data.RemainingBytes() < kNextNodePositionMaxDataSize * 2) + return 0; + AXNodePositionFuzzerGenerator position_fuzzer(tree, max_id, fuzz_data); + + // Having two positions allows us to test "more interesting" APIs that do work + // on multiple positions. + TestPositionType previous_position = position_fuzzer.CreateNewPosition(); + TestPositionType position = position_fuzzer.CreateNewPosition(); + + while (fuzz_data.RemainingBytes() > kNextNodePositionMaxDataSize) { + // Run with --v=1 to aid in debugging a specific crash. + VLOG(1) << position->ToString() << fuzz_data.RemainingBytes(); + + position_fuzzer.CallPositionAPIs(position, previous_position); + + // Determine next position to test: + TestPositionType next_position = + position_fuzzer.GenerateNextPosition(position, previous_position); + previous_position = std::move(position); + position = std::move(next_position); + } + + return 0; +} diff --git a/chromium/ui/accessibility/ax_node_position_unittest.cc b/chromium/ui/accessibility/ax_node_position_unittest.cc index 32c2f162137..c63083f407d 100644 --- a/chromium/ui/accessibility/ax_node_position_unittest.cc +++ b/chromium/ui/accessibility/ax_node_position_unittest.cc @@ -125,7 +125,7 @@ class AXPositionTest : public ::testing::Test, public TestAXTreeManager { AXNodeData inline_box2_; private: - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behaviour_; + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behaviour_; // Manages a minimalistic Views tree that is hosting the test webpage. TestAXTreeManager views_tree_manager_; }; @@ -3032,7 +3032,7 @@ TEST_F(AXPositionTest, AtStartOrEndOfParagraphWithIgnoredNodes) { } TEST_F(AXPositionTest, AtStartOrEndOfParagraphWithEmbeddedObjectCharacter) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // This test ensures that "At{Start|End}OfParagraph" work correctly when there @@ -4088,7 +4088,7 @@ TEST_F(AXPositionTest, AsLeafTextPositionWithTextPositionAndEmptyTextSandwich) { AXNodeData button_data; button_data.id = 3; button_data.role = ax::mojom::Role::kButton; - button_data.SetName(""); + button_data.SetNameExplicitlyEmpty(); button_data.SetNameFrom(ax::mojom::NameFrom::kContents); AXNodeData more_text_data; @@ -4131,7 +4131,7 @@ TEST_F(AXPositionTest, AsLeafTextPositionWithTextPositionAndEmptyTextSandwich) { } TEST_F(AXPositionTest, AsLeafTextPositionWithTextPositionAndEmbeddedObject) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea "<embedded_object><embedded_object>" @@ -4883,7 +4883,7 @@ TEST_F(AXPositionTest, CreatePositionAtPreviousFormatStartWithNullPosition) { } TEST_F(AXPositionTest, CreatePositionAtPreviousFormatStartWithTreePosition) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); TestPositionType tree_position = AXNodePosition::CreateTreePosition( GetTreeID(), static_text1_.id, 1 /* child_index */); @@ -4943,7 +4943,7 @@ TEST_F(AXPositionTest, CreatePositionAtPreviousFormatStartWithTreePosition) { } TEST_F(AXPositionTest, CreatePositionAtPreviousFormatStartWithTextPosition) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); TestPositionType text_position = AXNodePosition::CreateTextPosition( GetTreeID(), inline_box1_.id, 2 /* text_offset */, @@ -5020,7 +5020,7 @@ TEST_F(AXPositionTest, CreatePositionAtNextFormatEndWithNullPosition) { } TEST_F(AXPositionTest, CreatePositionAtNextFormatEndWithTreePosition) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); TestPositionType tree_position = AXNodePosition::CreateTreePosition( GetTreeID(), button_.id, 0 /* child_index */); @@ -5087,7 +5087,7 @@ TEST_F(AXPositionTest, CreatePositionAtNextFormatEndWithTreePosition) { } TEST_F(AXPositionTest, CreatePositionAtNextFormatEndWithTextPosition) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); TestPositionType text_position = AXNodePosition::CreateTextPosition( GetTreeID(), button_.id, 0 /* text_offset */, @@ -5163,7 +5163,7 @@ TEST_F(AXPositionTest, CreatePositionAtNextFormatEndWithTextPosition) { } TEST_F(AXPositionTest, CreatePositionAtNextFormatEndOnEmbeddedObject) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++root_1 // ++++heading_2 @@ -7772,7 +7772,7 @@ TEST_F(AXPositionTest, CreateParentPositionWithMoveDirection) { // This test only applies when "object replacement characters" are used in the // accessibility tree, e.g., in IAccessible2, UI Automation and Linux ATK // APIs. - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // This test ensures that "CreateParentPosition" (and by extension @@ -8632,7 +8632,7 @@ TEST_F(AXPositionTest, CreateParentAndLeafPositionWithEmptyNodes) { } TEST_F(AXPositionTest, CreateParentAndLeafPositionWithEmbeddedObjects) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++kRootWebArea "<embedded>Hello<embedded>" @@ -9653,7 +9653,7 @@ TEST_F(AXPositionTest, AsValidPosition) { } TEST_F(AXPositionTest, AsValidPositionInDescendantOfEmptyObject) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea @@ -10538,7 +10538,7 @@ TEST_F(AXPositionTest, OperatorEqualsSameTextOffsetDifferentAnchorIdLeaf) { } TEST_F(AXPositionTest, OperatorEqualsTextPositionsInTextField) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea @@ -10603,7 +10603,7 @@ TEST_F(AXPositionTest, OperatorEqualsTextPositionsInTextField) { } TEST_F(AXPositionTest, OperatorEqualsTextPositionsInSearchBox) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea @@ -10705,7 +10705,7 @@ TEST_F(AXPositionTest, OperatorEqualsTextPositionsInSearchBox) { } TEST_F(AXPositionTest, OperatorsTreePositionsAroundEmbeddedCharacter) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea "<embedded_object><embedded_object>" @@ -10839,7 +10839,7 @@ TEST_F(AXPositionTest, OperatorsTreePositionsAroundEmbeddedCharacter) { } TEST_F(AXPositionTest, OperatorsTextPositionsAroundEmbeddedCharacter) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea "<embedded_object><embedded_object>" @@ -11275,7 +11275,7 @@ TEST_F(AXPositionTest, CreateNextAnchorPosition) { AXNodeData empty_text_data; empty_text_data.id = 4; empty_text_data.role = ax::mojom::Role::kStaticText; - empty_text_data.SetName(""); + empty_text_data.SetNameExplicitlyEmpty(); AXNodeData more_text_data; more_text_data.id = 5; @@ -11765,7 +11765,7 @@ TEST_F(AXPositionTest, CreatePreviousWordPositionInList) { } TEST_F(AXPositionTest, EmptyObjectReplacedByCharacterTextNavigation) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea @@ -12100,7 +12100,7 @@ TEST_F(AXPositionTest, EmptyObjectReplacedByCharacterTextNavigation) { } TEST_F(AXPositionTest, EmptyObjectReplacedByCharacterEmbedObject) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // Parent Tree @@ -12152,7 +12152,7 @@ TEST_F(AXPositionTest, TextNavigationWithCollapsedCombobox) { // collapsed, the subtree of that combobox needs to be hidden and, when // expanded, it must be accessible in the tree. This test ensures we can't // navigate into the options of a collapsed menu list popup. - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); // ++1 kRootWebArea @@ -12314,7 +12314,7 @@ TEST_F(AXPositionTest, TextNavigationWithCollapsedCombobox) { } TEST_F(AXPositionTest, GetUnignoredSelectionWithLeafNodes) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kExposeCharacter); AXNodeData root_data; diff --git a/chromium/ui/accessibility/ax_node_unittest.cc b/chromium/ui/accessibility/ax_node_unittest.cc index bd6674a2ac9..73553698b8f 100644 --- a/chromium/ui/accessibility/ax_node_unittest.cc +++ b/chromium/ui/accessibility/ax_node_unittest.cc @@ -11,6 +11,7 @@ #include "testing/gmock/include/gmock/gmock-matchers.h" #include "testing/gtest/include/gtest/gtest.h" +#include "ui/accessibility/ax_enums.mojom-shared.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/ax_node_data.h" #include "ui/accessibility/ax_position.h" @@ -18,6 +19,7 @@ #include "ui/accessibility/ax_tree_data.h" #include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/test_ax_tree_manager.h" +#include "ui/gfx/geometry/rect_f.h" namespace ui { @@ -396,7 +398,7 @@ TEST(AXNodeTest, TreeWalkingCrossingTreeBoundary) { } TEST(AXNodeTest, GetValueForControlTextField) { - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( AXEmbeddedObjectBehavior::kSuppressCharacter); // kRootWebArea @@ -631,6 +633,112 @@ TEST(AXNodeTest, GetLowestPlatformAncestor) { EXPECT_EQ(text_field_node, inline_box_2_node->GetLowestPlatformAncestor()); } +TEST(AXNodeTest, GetTextContentRangeBounds) { + constexpr char16_t kEnglishText[] = u"Hey"; + const std::vector<int32_t> kEnglishCharacterOffsets = {12, 19, 27}; + // A Hindi word (which means "Hindi") consisting of two letters. + constexpr char16_t kHindiText[] = u"\x0939\x093F\x0928\x094D\x0926\x0940"; + const std::vector<int32_t> kHindiCharacterOffsets = {40, 40, 59, 59, 59, 59}; + // A Thai word (which means "feel") consisting of 3 letters. + constexpr char16_t kThaiText[] = u"\x0E23\x0E39\x0E49\x0E2A\x0E36\x0E01"; + const std::vector<int32_t> kThaiCharacterOffsets = {66, 66, 66, 76, 76, 85}; + + AXNodeData root_data; + root_data.id = 1; + root_data.role = ax::mojom::Role::kRootWebArea; + + AXNodeData text_data1; + text_data1.id = 2; + text_data1.role = ax::mojom::Role::kStaticText; + text_data1.SetName(kEnglishText); + text_data1.AddIntAttribute( + ax::mojom::IntAttribute::kTextDirection, + static_cast<int32_t>(ax::mojom::WritingDirection::kLtr)); + text_data1.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + kEnglishCharacterOffsets); + + AXNodeData text_data2; + text_data2.id = 3; + text_data2.role = ax::mojom::Role::kStaticText; + text_data2.SetName(kHindiText); + text_data2.AddIntAttribute( + ax::mojom::IntAttribute::kTextDirection, + static_cast<int32_t>(ax::mojom::WritingDirection::kRtl)); + text_data2.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + kHindiCharacterOffsets); + + AXNodeData text_data3; + text_data3.id = 4; + text_data3.role = ax::mojom::Role::kStaticText; + text_data3.SetName(kThaiText); + text_data3.AddIntAttribute( + ax::mojom::IntAttribute::kTextDirection, + static_cast<int32_t>(ax::mojom::WritingDirection::kTtb)); + text_data3.AddIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets, + kThaiCharacterOffsets); + + root_data.child_ids = {text_data1.id, text_data2.id, text_data3.id}; + + AXTreeUpdate update; + update.root_id = root_data.id; + update.nodes = {root_data, text_data1, text_data2, text_data3}; + update.has_tree_data = true; + + AXTreeData tree_data; + tree_data.tree_id = AXTreeID::CreateNewAXTreeID(); + tree_data.title = "Application"; + update.tree_data = tree_data; + + AXTree tree; + ASSERT_TRUE(tree.Unserialize(update)) << tree.error(); + + const AXNode* root_node = tree.root(); + ASSERT_EQ(root_data.id, root_node->id()); + + const AXNode* text1_node = root_node->GetUnignoredChildAtIndex(0); + ASSERT_EQ(text_data1.id, text1_node->id()); + const AXNode* text2_node = root_node->GetUnignoredChildAtIndex(1); + ASSERT_EQ(text_data2.id, text2_node->id()); + const AXNode* text3_node = root_node->GetUnignoredChildAtIndex(2); + ASSERT_EQ(text_data3.id, text3_node->id()); + + // Bounds should be the same between UTF-8 and UTF-16 for `kEnglishText`. + EXPECT_EQ(gfx::RectF(0, 0, 27, 0), + text1_node->GetTextContentRangeBoundsUTF8(0, 3)); + EXPECT_EQ(gfx::RectF(12, 0, 7, 0), + text1_node->GetTextContentRangeBoundsUTF8(1, 2)); + EXPECT_EQ(gfx::RectF(), text1_node->GetTextContentRangeBoundsUTF8(2, 4)); + EXPECT_EQ(gfx::RectF(0, 0, 27, 0), + text1_node->GetTextContentRangeBoundsUTF16(0, 3)); + EXPECT_EQ(gfx::RectF(12, 0, 7, 0), + text1_node->GetTextContentRangeBoundsUTF16(1, 2)); + EXPECT_EQ(gfx::RectF(), text1_node->GetTextContentRangeBoundsUTF16(2, 4)); + + // Offsets are manually converted between UTF-8 and UTF-16. + // + // `kHindiText` is 6 code units in UTF-16 and 18 in UTF-8. + EXPECT_EQ(gfx::RectF(0, 0, 59, 0), + text2_node->GetTextContentRangeBoundsUTF8(0, 18)); + EXPECT_EQ(gfx::RectF(0, 0, 19, 0), + text2_node->GetTextContentRangeBoundsUTF8(6, 12)); + EXPECT_EQ(gfx::RectF(0, 0, 59, 0), + text2_node->GetTextContentRangeBoundsUTF16(0, 6)); + EXPECT_EQ(gfx::RectF(0, 0, 19, 0), + text2_node->GetTextContentRangeBoundsUTF16(2, 4)); + + // Offsets are manually converted between UTF-8 and UTF-16. + // + // `kThaiText` is 6 code units in UTF-16 and 18 in UTF-8. + EXPECT_EQ(gfx::RectF(0, 0, 0, 85), + text3_node->GetTextContentRangeBoundsUTF8(0, 18)); + EXPECT_EQ(gfx::RectF(0, 66, 0, 10), + text3_node->GetTextContentRangeBoundsUTF8(6, 12)); + EXPECT_EQ(gfx::RectF(0, 0, 0, 85), + text3_node->GetTextContentRangeBoundsUTF16(0, 6)); + EXPECT_EQ(gfx::RectF(0, 66, 0, 10), + text3_node->GetTextContentRangeBoundsUTF16(2, 4)); +} + TEST(AXNodeTest, IsGridCellReadOnlyOrDisabled) { // ++kRootWebArea // ++++kGrid diff --git a/chromium/ui/accessibility/ax_position.h b/chromium/ui/accessibility/ax_position.h index 13f38045eaf..3ada31c07f1 100644 --- a/chromium/ui/accessibility/ax_position.h +++ b/chromium/ui/accessibility/ax_position.h @@ -35,7 +35,6 @@ #include "ui/accessibility/ax_text_attributes.h" #include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/ax_tree_manager.h" -#include "ui/accessibility/ax_tree_manager_map.h" #include "ui/gfx/utf16_indexing.h" namespace ui { @@ -165,8 +164,6 @@ enum class AXEmbeddedObjectBehavior { // TODO(crbug.com/1204592) Don't export this so tests can't change it. extern AX_EXPORT AXEmbeddedObjectBehavior g_ax_embedded_object_behavior; -namespace testing { - class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter { public: explicit ScopedAXEmbeddedObjectBehaviorSetter( @@ -177,8 +174,6 @@ class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter { AXEmbeddedObjectBehavior prev_behavior_; }; -} // namespace testing - // Forward declarations. template <class AXPositionType, class AXNodeType> class AXPosition; @@ -406,7 +401,7 @@ class AXPosition { DCHECK(GetManager()); std::ostringstream str; str << "* Position: " << ToString() - << "\n* Manager: " << GetManager()->ToString() + << "\n* Manager: " << GetManager()->ax_tree()->data().ToString() << "\n* Anchor node: " << *GetAnchor(); return str.str(); } @@ -415,9 +410,7 @@ class AXPosition { AXTreeID tree_id() const { return tree_id_; } AXNodeID anchor_id() const { return anchor_id_; } - AXTreeManager* GetManager() const { - return AXTreeManagerMap::GetInstance().GetManager(tree_id()); - } + AXTreeManager* GetManager() const { return AXTreeManager::FromID(tree_id()); } AXNode* GetAnchor() const { if (tree_id_ == AXTreeIDUnknown() || anchor_id_ == kInvalidAXNodeID) diff --git a/chromium/ui/accessibility/ax_range_unittest.cc b/chromium/ui/accessibility/ax_range_unittest.cc index 4f0182e4e09..918b615e390 100644 --- a/chromium/ui/accessibility/ax_range_unittest.cc +++ b/chromium/ui/accessibility/ax_range_unittest.cc @@ -145,7 +145,7 @@ class AXRangeTest : public ::testing::Test, public TestAXTreeManager { AXNodeData empty_paragraph_; private: - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior_; + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior_; }; // These tests use kSuppressCharacter behavior. diff --git a/chromium/ui/accessibility/ax_role_properties.cc b/chromium/ui/accessibility/ax_role_properties.cc index 1141967da8b..ae176cfbece 100644 --- a/chromium/ui/accessibility/ax_role_properties.cc +++ b/chromium/ui/accessibility/ax_role_properties.cc @@ -105,8 +105,10 @@ bool IsCellOrTableHeader(const ax::mojom::Role role) { bool IsChildTreeOwner(const ax::mojom::Role role) { switch (role) { + case ax::mojom::Role::kEmbeddedObject: case ax::mojom::Role::kIframe: case ax::mojom::Role::kIframePresentational: + case ax::mojom::Role::kPluginObject: case ax::mojom::Role::kPortal: return true; default: @@ -547,7 +549,6 @@ bool IsReadOnlySupported(const ax::mojom::Role role) { case ax::mojom::Role::kSwitch: case ax::mojom::Role::kTextField: case ax::mojom::Role::kTextFieldWithComboBox: - case ax::mojom::Role::kToggleButton: case ax::mojom::Role::kTreeGrid: return true; diff --git a/chromium/ui/accessibility/ax_role_properties.h b/chromium/ui/accessibility/ax_role_properties.h index 0ab00426668..2165f8d65f9 100644 --- a/chromium/ui/accessibility/ax_role_properties.h +++ b/chromium/ui/accessibility/ax_role_properties.h @@ -41,6 +41,8 @@ AX_BASE_EXPORT bool IsButton(const ax::mojom::Role role); AX_BASE_EXPORT bool IsCellOrTableHeader(const ax::mojom::Role role); // Returns true if the role is expected to be the parent of a child tree. +// Can return false for a child tree owner if an ARIA role was used, e.g. +// <iframe role="region">. AX_BASE_EXPORT bool IsChildTreeOwner(const ax::mojom::Role role); // Returns true if the provided role belongs to an object on which a click diff --git a/chromium/ui/accessibility/ax_tree.cc b/chromium/ui/accessibility/ax_tree.cc index 5fc55901e6c..9e716fc9c10 100644 --- a/chromium/ui/accessibility/ax_tree.cc +++ b/chromium/ui/accessibility/ax_tree.cc @@ -802,9 +802,9 @@ void AXTree::Destroy() { { ScopedTreeUpdateInProgressStateSetter tree_update_in_progress(*this); - - DestroyNodeAndSubtree(root_, nullptr); - root_ = nullptr; + // ExtractAsDangling clears the underlying pointer and returns another + // raw_ptr instance that is allowed to dangle. + DestroyNodeAndSubtree(root_.ExtractAsDangling(), nullptr); } // tree_update_in_progress. } @@ -822,6 +822,7 @@ gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, gfx::RectF bounds, bool* offscreen, bool clip_bounds, + bool skip_container_offset, bool allow_recursion) const { // If |bounds| is uninitialized, which is not the same as empty, // start with the node bounds. @@ -838,16 +839,17 @@ gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, ui::AXNode* child = node->children()[i]; bool ignore_offscreen; - gfx::RectF child_bounds = RelativeToTreeBoundsInternal( - child, gfx::RectF(), &ignore_offscreen, clip_bounds, - /* allow_recursion = */ false); + gfx::RectF child_bounds = + RelativeToTreeBoundsInternal(child, gfx::RectF(), &ignore_offscreen, + clip_bounds, skip_container_offset, + /* allow_recursion = */ false); bounds.Union(child_bounds); } if (bounds.width() > 0 && bounds.height() > 0) { return bounds; } } - } else { + } else if (!skip_container_offset) { bounds.Offset(node->data().relative_bounds.bounds.x(), node->data().relative_bounds.bounds.y()); } @@ -863,7 +865,7 @@ gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, GetFromId(node->data().relative_bounds.offset_container_id); if (!container && container != root()) container = root(); - if (!container || container == node) + if (!container || container == node || skip_container_offset) break; gfx::RectF container_bounds = container->data().relative_bounds.bounds; @@ -950,6 +952,7 @@ gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, bool ignore_offscreen; ancestor_bounds = RelativeToTreeBoundsInternal( ancestor, gfx::RectF(), &ignore_offscreen, clip_bounds, + skip_container_offset, /* allow_recursion = */ false); gfx::RectF original_bounds = original_node->data().relative_bounds.bounds; @@ -971,10 +974,11 @@ gfx::RectF AXTree::RelativeToTreeBoundsInternal(const AXNode* node, gfx::RectF AXTree::RelativeToTreeBounds(const AXNode* node, gfx::RectF bounds, bool* offscreen, - bool clip_bounds) const { + bool clip_bounds, + bool skip_container_offset) const { bool allow_recursion = true; return RelativeToTreeBoundsInternal(node, bounds, offscreen, clip_bounds, - allow_recursion); + skip_container_offset, allow_recursion); } gfx::RectF AXTree::GetTreeBounds(const AXNode* node, @@ -1167,7 +1171,7 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { if (!root_) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kNoRoot); - RecordError("Tree has no root."); + RecordError(update_state, "Tree has no root."); return false; } @@ -1526,8 +1530,10 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, if (!is_new_root) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kNotInTree); - RecordError(base::StringPrintf( - "%d will not be in the tree and is not the new root", new_data.id)); + RecordError(*update_state, + base::StringPrintf( + "%d will not be in the tree and is not the new root", + new_data.id)); return false; } @@ -1537,9 +1543,11 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, absl::nullopt)) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kCreationPending); - RecordError(base::StringPrintf( - "Node %d is already pending for creation, cannot be the new root", - new_data.id)); + RecordError( + *update_state, + base::StringPrintf( + "Node %d is already pending for creation, cannot be the new root", + new_data.id)); return false; } if (update_state->pending_root_id) { @@ -1555,7 +1563,8 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, if (base::Contains(new_child_id_set, new_child_id)) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kDuplicateChild); - RecordError(base::StringPrintf("Node %d has duplicate child id %d", + RecordError(*update_state, + base::StringPrintf("Node %d has duplicate child id %d", new_data.id, new_child_id)); return false; } @@ -1580,9 +1589,10 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, new_data.id)) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kCreationPendingForChild); - RecordError(base::StringPrintf( - "Node %d is already pending for creation, cannot be a new child", - child_id)); + RecordError(*update_state, + base::StringPrintf("Node %d is already pending for " + "creation, cannot be a new child", + child_id)); return false; } } @@ -1624,9 +1634,10 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, if (update_state->ShouldPendingNodeExistInTree(child_id)) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kReparent); - RecordError(base::StringPrintf( - "Node %d is not marked for destruction, would be reparented to %d", - child_id, new_data.id)); + RecordError(*update_state, + base::StringPrintf("Node %d is not marked for destruction, " + "would be reparented to %d", + child_id, new_data.id)); return false; } @@ -1637,9 +1648,10 @@ bool AXTree::ComputePendingChangesToNode(const AXNodeData& new_data, new_data.id)) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kCreationPendingForChild); - RecordError(base::StringPrintf( - "Node %d is already pending for creation, cannot be a new child", - child_id)); + RecordError(*update_state, + base::StringPrintf("Node %d is already pending for " + "creation, cannot be a new child", + child_id)); return false; } } else { @@ -1678,8 +1690,9 @@ bool AXTree::UpdateNode(const AXNodeData& src, if (!is_new_root) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kNotInTree); - RecordError(base::StringPrintf( - "%d is not in the tree and not the new root", src.id)); + RecordError(*update_state, + base::StringPrintf( + "%d is not in the tree and not the new root", src.id)); return false; } @@ -2003,7 +2016,7 @@ bool AXTree::ValidatePendingChangesComplete( std::string error = "Nodes left pending by the update:"; for (const AXNodeID pending_id : update_state.pending_node_ids) error += base::StringPrintf(" %d", pending_id); - RecordError(error); + RecordError(update_state, error); return false; } @@ -2029,11 +2042,13 @@ bool AXTree::ValidatePendingChangesComplete( if (has_pending_changes) { ACCESSIBILITY_TREE_UNSERIALIZE_ERROR_HISTOGRAM( AXTreeUnserializeError::kPendingChanges); - RecordError(base::StringPrintf( - "Changes left pending by the update; " - "destroy subtrees: %s, destroy nodes: %s, create nodes: %s", - destroy_subtree_ids.c_str(), destroy_node_ids.c_str(), - create_node_ids.c_str())); + RecordError( + update_state, + base::StringPrintf( + "Changes left pending by the update; " + "destroy subtrees: %s, destroy nodes: %s, create nodes: %s", + destroy_subtree_ids.c_str(), destroy_node_ids.c_str(), + create_node_ids.c_str())); } return !has_pending_changes; } @@ -2142,7 +2157,8 @@ bool AXTree::CreateNewChildVector(AXNode* node, // If this case occurs, continue so this node isn't left in an // inconsistent state, but return failure at the end. if (child->parent()) { - RecordError(base::StringPrintf("Node %d reparented from %d to %d", + RecordError(*update_state, + base::StringPrintf("Node %d reparented from %d to %d", child->id(), child->parent()->id(), node->id())); } else { @@ -2668,12 +2684,15 @@ bool ComputeUnignoredSelectionEndpoint( } // namespace +AXTree::Selection AXTree::GetSelection() const { + return {data().sel_is_backward, data().sel_anchor_object_id, + data().sel_anchor_offset, data().sel_anchor_affinity, + data().sel_focus_object_id, data().sel_focus_offset, + data().sel_focus_affinity}; +} + AXTree::Selection AXTree::GetUnignoredSelection() const { - Selection unignored_selection = { - data().sel_is_backward, data().sel_anchor_object_id, - data().sel_anchor_offset, data().sel_anchor_affinity, - data().sel_focus_object_id, data().sel_focus_offset, - data().sel_focus_affinity}; + Selection unignored_selection = GetSelection(); // If one of the selection endpoints is invalid, then the other endpoint // should also be unset. @@ -2724,18 +2743,36 @@ void AXTree::NotifyTreeManagerWillBeRemoved(AXTreeID previous_tree_id) { observer.OnTreeManagerWillBeRemoved(previous_tree_id); } -void AXTree::RecordError(std::string new_error) { +void AXTree::RecordError(const AXTreeUpdateState& update_state, + std::string new_error) { if (!error_.empty()) error_ = error_ + "\n"; // Add visual separation between errors. error_ = error_ + new_error; - if (!error_.empty()) { - // Add a crash key so we can figure out why this is happening. - static crash_reporter::CrashKeyString<256> ax_tree_error( - "ax_tree_unserialize_error"); - ax_tree_error.Set(error_); - LOG(ERROR) << error_; - } + LOG(ERROR) << new_error; + + static auto* const ax_tree_error_key = base::debug::AllocateCrashKeyString( + "ax_tree_error", base::debug::CrashKeySize::Size256); + static auto* const ax_tree_update_key = base::debug::AllocateCrashKeyString( + "ax_tree_update", base::debug::CrashKeySize::Size256); + static auto* const ax_tree_key = base::debug::AllocateCrashKeyString( + "ax_tree", base::debug::CrashKeySize::Size256); + static auto* const ax_tree_data_key = base::debug::AllocateCrashKeyString( + "ax_tree_data", base::debug::CrashKeySize::Size256); + + // Log additional crash keys so we can debug bad tree updates. + base::debug::SetCrashKeyString(ax_tree_error_key, new_error); + base::debug::SetCrashKeyString(ax_tree_update_key, + update_state.pending_tree_update.ToString()); + base::debug::SetCrashKeyString(ax_tree_key, TreeToStringHelper(root_, 1)); + base::debug::SetCrashKeyString(ax_tree_data_key, data().ToString()); + + // In fast-failing-builds, crash immediately with a message, otherwise + // rely on AccessibilityFatalError(), which will not crash until multiple + // errors occur. + SANITIZER_NOTREACHED() << new_error << "\n" + << update_state.pending_tree_update.ToString() << "\n" + << ToString(); } } // namespace ui diff --git a/chromium/ui/accessibility/ax_tree.h b/chromium/ui/accessibility/ax_tree.h index 5bf1e1b818d..554c202e5ec 100644 --- a/chromium/ui/accessibility/ax_tree.h +++ b/chromium/ui/accessibility/ax_tree.h @@ -11,9 +11,10 @@ #include <memory> #include <set> #include <string> -#include <unordered_map> #include <vector> +#include "base/containers/flat_map.h" +#include "base/debug/crash_logging.h" #include "base/memory/raw_ptr.h" #include "base/metrics/histogram_functions.h" #include "base/observer_list.h" @@ -146,7 +147,8 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { gfx::RectF RelativeToTreeBounds(const AXNode* node, gfx::RectF node_bounds, bool* offscreen = nullptr, - bool clip_bounds = true) const; + bool clip_bounds = true, + bool skip_container_offset = false) const; // Get the bounds of a node in the coordinate space of the tree. // If set, updates |offscreen| boolean to be true if the node is offscreen @@ -213,6 +215,14 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // present in the cache. absl::optional<int> GetSetSize(const AXNode& node) override; + // Returns the part of the current selection that falls within this + // accessibility tree, if any. + Selection GetSelection() const override; + + // Returns the part of the current selection that falls within this + // accessibility tree, if any, adjusting its endpoints to be within unignored + // nodes. (An "ignored" node is a node that is not exposed to platform APIs: + // See `AXNode::IsIgnored`.) Selection GetUnignoredSelection() const override; bool GetTreeUpdateInProgressState() const override; @@ -246,7 +256,9 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // Accumulate errors as there can be more than one before Chrome is crashed // via AccessibilityFatalError(); - void RecordError(std::string new_error); + // In an AX_FAIL_FAST_BUILD, will assert/crash immediately. + void RecordError(const AXTreeUpdateState& update_state, + std::string new_error); // AXNode::OwnerTree override. // @@ -384,13 +396,14 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { gfx::RectF node_bounds, bool* offscreen, bool clip_bounds, + bool skip_container_offset, bool allow_recursion) const; base::ObserverList<AXTreeObserver> observers_; raw_ptr<AXNode> root_ = nullptr; - std::unordered_map<AXNodeID, std::unique_ptr<AXNode>> id_map_; std::string error_; AXTreeData data_; + base::flat_map<AXNodeID, std::unique_ptr<AXNode>> id_map_; // Map from an int attribute (if IsNodeIdIntAttribute is true) to // a reverse mapping from target nodes to source nodes. @@ -403,7 +416,7 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // Map from node ID to cached table info, if the given node is a table. // Invalidated every time the tree is updated. - mutable std::unordered_map<AXNodeID, std::unique_ptr<AXTableInfo>> + mutable base::flat_map<AXNodeID, std::unique_ptr<AXTableInfo>> table_info_map_; // The next negative node ID to use for internal nodes. @@ -462,7 +475,7 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { // objects. // All other objects will map to default-constructed OrderedSetInfo objects. // Invalidated every time the tree is updated. - mutable std::unordered_map<AXNodeID, NodeSetSizePosInSetInfo> + mutable base::flat_map<AXNodeID, NodeSetSizePosInSetInfo> node_set_size_pos_in_set_info_map_; // Indicates if the tree is updating. diff --git a/chromium/ui/accessibility/ax_tree_fuzzer_util.cc b/chromium/ui/accessibility/ax_tree_fuzzer_util.cc new file mode 100644 index 00000000000..ed27dc808ec --- /dev/null +++ b/chromium/ui/accessibility/ax_tree_fuzzer_util.cc @@ -0,0 +1,355 @@ +// Copyright 2022 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 "ui/accessibility/ax_tree_fuzzer_util.h" + +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_node_data.h" +#include "ui/accessibility/ax_node_position.h" +#include "ui/accessibility/ax_range.h" +#include "ui/accessibility/ax_role_properties.h" +#include "ui/accessibility/ax_tree.h" +#include "ui/accessibility/ax_tree_data.h" +#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree_update.h" + +FuzzerData::FuzzerData(const unsigned char* data, size_t size) + : data_(data), data_size_(size), data_index_(0) {} + +size_t FuzzerData::RemainingBytes() { + return data_size_ - data_index_; +} + +unsigned char FuzzerData::NextByte() { + CHECK(RemainingBytes()); + return data_[data_index_++]; +} + +const unsigned char* FuzzerData::NextBytes(size_t amount) { + CHECK(RemainingBytes() >= amount); + const unsigned char* current_position = &data_[data_index_]; + data_index_ += amount; + return current_position; +} + +ui::AXTree* AXTreeFuzzerGenerator::GetTree() { + return tree_manager_.GetTree(); +} + +void AXTreeFuzzerGenerator::GenerateInitialUpdate(FuzzerData& fuzz_data, + int node_count) { + max_assigned_node_id_ = 1; + ui::AXTreeUpdate initial_state; + initial_state.root_id = max_assigned_node_id_++; + + initial_state.has_tree_data = true; + initial_state.tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + + ui::AXNodeData root; + root.id = initial_state.root_id; + root.role = ax::mojom::Role::kRootWebArea; + + std::stack<size_t> parent_index_stack; + parent_index_stack.push(initial_state.nodes.size()); + initial_state.nodes.push_back(root); + + // As we give out ids sequentially, starting at 1, the + // ...max_assigned_node_id_... is equivalent to the node count. + while (fuzz_data.RemainingBytes() >= kMinimumNewNodeFuzzDataSize && + max_assigned_node_id_ < node_count) { + size_t extra_data_size = + fuzz_data.RemainingBytes() - kMinimumNewNodeFuzzDataSize; + + ui::AXNodeData& parent = initial_state.nodes[parent_index_stack.top()]; + + // Create a node. + ui::AXNodeData node = CreateChildNodeData(parent, max_assigned_node_id_++); + + // Determine role. + node.role = GetInterestingRole(fuzz_data.NextByte(), parent.role); + + // Add role-specific properties. + AddRoleSpecificProperties( + fuzz_data, node, + parent.GetStringAttribute(ax::mojom::StringAttribute::kName), + extra_data_size); + + // Determine the relationship of the next node from fuzz data. See + // implementation of `DetermineNextNodeRelationship` for details. + size_t ancestor_pop_count; + switch (DetermineNextNodeRelationship(node.role, fuzz_data.NextByte())) { + case kChild: + CHECK(CanHaveChildren(node.role)); + parent_index_stack.push(initial_state.nodes.size()); + break; + case kSibling: + initial_state.nodes.push_back(node); + break; + case kSiblingToAncestor: + ancestor_pop_count = 1 + fuzz_data.NextByte() % kMaxAncestorPopCount; + for (size_t i = 0; + i < ancestor_pop_count && parent_index_stack.size() > 1; ++i) { + parent_index_stack.pop(); + } + break; + } + + initial_state.nodes.push_back(node); + } + // Run with --v=1 to aid in debugging a specific crash. + VLOG(1) << "Input accessibility tree:\n" << initial_state.ToString(); + tree_manager_.SetTree(std::make_unique<ui::AXTree>(initial_state)); +} + +// Pre-order depth first walk of tree. Skip over deleted subtrees. +void AXTreeFuzzerGenerator::RecursiveGenerateUpdate( + const ui::AXNode* node, + ui::AXTreeUpdate& tree_update, + FuzzerData& fuzz_data, + std::set<ui::AXNodeID>& updated_nodes) { + // Stop traversing if we run out of fuzz data. + if (fuzz_data.RemainingBytes() <= kMinimumNewNodeFuzzDataSize) + return; + size_t extra_data_size = + fuzz_data.RemainingBytes() - kMinimumNewNodeFuzzDataSize; + + AXTreeFuzzerGenerator::TreeUpdateOperation operation = kNoOperation; + if (!updated_nodes.count(node->id())) + operation = DetermineTreeUpdateOperation(node, fuzz_data.NextByte()); + + switch (operation) { + case kAddChild: { + // Determine where to insert the node. + // Create node and attach to parent. + ui::AXNodeData parent = node->data(); + ui::AXNodeData child = + CreateChildNodeData(parent, max_assigned_node_id_++); + + // Determine role. + child.role = GetInterestingRole(fuzz_data.NextByte(), node->GetRole()); + + // Add role-specific properties. + AddRoleSpecificProperties( + fuzz_data, child, + node->GetStringAttribute(ax::mojom::StringAttribute::kName), + extra_data_size); + // Also add inline text child if we can. + ui::AXNodeData inline_text_data; + if (ui::CanHaveInlineTextBoxChildren(child.role)) { + inline_text_data = CreateChildNodeData(child, max_assigned_node_id_++); + inline_text_data.role = ax::mojom::Role::kInlineTextBox; + inline_text_data.SetName( + child.GetStringAttribute(ax::mojom::StringAttribute::kName)); + } + // Add both the current node (parent) and the child to the tree update. + tree_update.nodes.push_back(parent); + tree_update.nodes.push_back(child); + updated_nodes.emplace(parent.id); + updated_nodes.emplace(child.id); + if (inline_text_data.id != ui::kInvalidAXNodeID) { + tree_update.nodes.push_back(inline_text_data); + updated_nodes.emplace(inline_text_data.id); + } + break; + } + case kRemoveNode: { + const ui::AXNode* parent = node->GetParent(); + if (updated_nodes.count(parent->id())) + break; + // Determine what node to delete. + // To delete a node, just find the parent and update the child list to + // no longer include this node. + ui::AXNodeData parent_update = parent->data(); + parent_update.child_ids.erase( + std::remove(parent_update.child_ids.begin(), + parent_update.child_ids.end(), node->id()), + parent_update.child_ids.end()); + tree_update.nodes.push_back(parent_update); + updated_nodes.emplace(parent_update.id); + + // This node was deleted, don't traverse to the subtree. + return; + } + case kTextChange: { + // Modify the text. + const ui::AXNode* child_inline_text = node->GetFirstChild(); + if (!child_inline_text || + child_inline_text->GetRole() != ax::mojom::Role::kInlineTextBox) { + break; + } + ui::AXNodeData static_text_data = node->data(); + ui::AXNodeData inline_text_data = child_inline_text->data(); + size_t text_size = + kMinTextFuzzDataSize + fuzz_data.NextByte() % kMaxTextFuzzDataSize; + if (text_size > extra_data_size) + text_size = extra_data_size; + extra_data_size -= text_size; + inline_text_data.SetName( + GenerateInterestingText(fuzz_data.NextBytes(text_size), text_size)); + static_text_data.SetName(inline_text_data.GetStringAttribute( + ax::mojom::StringAttribute::kName)); + tree_update.nodes.push_back(static_text_data); + tree_update.nodes.push_back(inline_text_data); + updated_nodes.emplace(static_text_data.id); + updated_nodes.emplace(inline_text_data.id); + break; + } + case kNoOperation: + break; + } + + // Visit subtree. + for (auto iter = node->AllChildrenBegin(); iter != node->AllChildrenEnd(); + ++iter) { + RecursiveGenerateUpdate(iter.get(), tree_update, fuzz_data, updated_nodes); + } +} + +// When building a tree update, we must take care to not create an +// unserializable tree. If the tree does not serialize, things like +// TestAXTreeObserver will not be able to handle the incorrectly serialized +// tree. This will require us to abort the fuzz run. +bool AXTreeFuzzerGenerator::GenerateTreeUpdate(FuzzerData& fuzz_data, + size_t node_count) { + ui::AXTreeUpdate tree_update; + std::set<ui::AXNodeID> updated_nodes; + RecursiveGenerateUpdate(tree_manager_.GetRootAsAXNode(), tree_update, + fuzz_data, updated_nodes); + return GetTree()->Unserialize(tree_update); +} + +ui::AXNodeID AXTreeFuzzerGenerator::GetMaxAssignedID() const { + return max_assigned_node_id_; +} + +ui::AXNodeData AXTreeFuzzerGenerator::CreateChildNodeData( + ui::AXNodeData& parent, + ui::AXNodeID new_node_id) { + ui::AXNodeData node; + node.id = new_node_id; + // Connect parent to this node. + parent.child_ids.push_back(node.id); + return node; +} + +// Determine the relationship of the next node from fuzz data. +AXTreeFuzzerGenerator::NextNodeRelationship +AXTreeFuzzerGenerator::DetermineNextNodeRelationship(ax::mojom::Role role, + unsigned char byte) { + // Force this to have a inline text child if it can. + if (ui::CanHaveInlineTextBoxChildren(role)) + return NextNodeRelationship::kChild; + + // Don't allow inline text boxes to have children or siblings. + if (role == ax::mojom::Role::kInlineTextBox) + return NextNodeRelationship::kSiblingToAncestor; + + // Determine next node using fuzz data. + NextNodeRelationship relationship = + static_cast<NextNodeRelationship>(byte % 3); + + // Check to ensure we can have children. + if (relationship == NextNodeRelationship::kChild && !CanHaveChildren(role)) { + return NextNodeRelationship::kSibling; + } + return relationship; +} + +AXTreeFuzzerGenerator::TreeUpdateOperation +AXTreeFuzzerGenerator::DetermineTreeUpdateOperation(const ui::AXNode* node, + unsigned char byte) { + switch (byte % 4) { + case 0: + // Don't delete the following nodes: + // 1) The root. TODO(janewman): implement root changes in an update. + // 2) Inline text. We don't want to leave Static text nodes without inline + // text children. + if (ax::mojom::Role::kRootWebArea != node->GetRole()) + return kRemoveNode; + ABSL_FALLTHROUGH_INTENDED; + case 1: + // Check to ensure this node can have children. Also consider that we + // shouldn't add children to static text, as these nodes only expect to + // have a inline text single child. + if (CanHaveChildren(node->GetRole()) && !ui::IsText(node->GetRole())) + return kAddChild; + ABSL_FALLTHROUGH_INTENDED; + case 2: + if (ax::mojom::Role::kStaticText == node->GetRole()) + return kTextChange; + ABSL_FALLTHROUGH_INTENDED; + default: + return kNoOperation; + } +} + +void AXTreeFuzzerGenerator::AddRoleSpecificProperties( + FuzzerData& fuzz_data, + ui::AXNodeData& node, + const std::string& parentName, + size_t extra_data_size) { + // TODO(janewman): Add ignored state. + // Add role-specific properties. + if (node.role == ax::mojom::Role::kInlineTextBox) { + node.SetName(parentName); + } else if (node.role == ax::mojom::Role::kLineBreak) { + node.SetName("\n"); + } else if (ui::IsText(node.role)) { + size_t text_size = + kMinTextFuzzDataSize + fuzz_data.NextByte() % kMaxTextFuzzDataSize; + if (text_size > extra_data_size) + text_size = extra_data_size; + extra_data_size -= text_size; + node.SetName( + GenerateInterestingText(fuzz_data.NextBytes(text_size), text_size)); + } +} + +ax::mojom::Role AXTreeFuzzerGenerator::GetInterestingRole( + unsigned char byte, + ax::mojom::Role parent_role) { + if (ui::CanHaveInlineTextBoxChildren(parent_role)) + return ax::mojom::Role::kInlineTextBox; + + // Bias towards creating text nodes so we end up with more text in the tree. + switch (byte % 7) { + default: + case 0: + case 1: + case 2: + return ax::mojom::Role::kStaticText; + case 3: + return ax::mojom::Role::kLineBreak; + case 4: + return ax::mojom::Role::kParagraph; + case 5: + return ax::mojom::Role::kGenericContainer; + case 6: + return ax::mojom::Role::kGroup; + } +} + +bool AXTreeFuzzerGenerator::CanHaveChildren(ax::mojom::Role role) { + switch (role) { + case ax::mojom::Role::kInlineTextBox: + return false; + default: + return true; + } +} + +std::u16string AXTreeFuzzerGenerator::GenerateInterestingText( + const unsigned char* data, + size_t size) { + std::u16string wide_str; + for (size_t i = 0; i + 1 < size; i += 2) { + char16_t char_16 = data[i] << 8; + char_16 |= data[i + 1]; + // Don't insert a null character. + if (char_16) + wide_str.push_back(char_16); + } + return wide_str; +} diff --git a/chromium/ui/accessibility/ax_tree_fuzzer_util.h b/chromium/ui/accessibility/ax_tree_fuzzer_util.h new file mode 100644 index 00000000000..b71af77e426 --- /dev/null +++ b/chromium/ui/accessibility/ax_tree_fuzzer_util.h @@ -0,0 +1,90 @@ +// Copyright 2022 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. +#ifndef UI_ACCESSIBILITY_AX_TREE_FUZZER_UTIL_H_ +#define UI_ACCESSIBILITY_AX_TREE_FUZZER_UTIL_H_ + +#include "ui/accessibility/ax_tree.h" +#include "ui/accessibility/ax_tree_data.h" +#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree_update.h" +#include "ui/accessibility/test_ax_tree_manager.h" + +// TODO(janewman): Replace usage with ...FuzzedDataProvider... +class FuzzerData { + public: + FuzzerData(const unsigned char* data, size_t size); + size_t RemainingBytes(); + unsigned char NextByte(); + const unsigned char* NextBytes(size_t amount); + + private: + const unsigned char* data_; + const size_t data_size_; + size_t data_index_; +}; + +class AXTreeFuzzerGenerator { + public: + AXTreeFuzzerGenerator() = default; + ~AXTreeFuzzerGenerator() = default; + + ui::AXTree* GetTree(); + + void GenerateInitialUpdate(FuzzerData& fuzz_data, int node_count); + bool GenerateTreeUpdate(FuzzerData& fuzz_data, size_t node_count); + + ui::AXNodeID GetMaxAssignedID() const; + + // This must be kept in sync with the minimum amount of data needed to create + // any node. Any optional node data should check to ensure there is space. + static constexpr size_t kMinimumNewNodeFuzzDataSize = 5; + static constexpr size_t kMinTextFuzzDataSize = 10; + static constexpr size_t kMaxTextFuzzDataSize = 200; + + // When creating a node, we allow for the next node to be a sibling of an + // ancestor, this constant determines the maximum nodes we will pop when + // building the tree. + static constexpr size_t kMaxAncestorPopCount = 3; + + private: + enum NextNodeRelationship { + // Next node is a child of this node. (This node is a parent.) + kChild, + // Next node is sibling to this node. (This node is a leaf.) + kSibling, + // Next node is sibling to an ancestor. (This node is a leaf.) + kSiblingToAncestor, + }; + enum TreeUpdateOperation { + kAddChild, + kRemoveNode, + kTextChange, + kNoOperation + }; + + void RecursiveGenerateUpdate(const ui::AXNode* node, + ui::AXTreeUpdate& tree_update, + FuzzerData& fuzz_data, + std::set<ui::AXNodeID>& updated_nodes); + // TODO(janewman): Many of these can be made static. + ui::AXNodeData CreateChildNodeData(ui::AXNodeData& parent, + ui::AXNodeID new_node_id); + NextNodeRelationship DetermineNextNodeRelationship(ax::mojom::Role role, + unsigned char byte); + TreeUpdateOperation DetermineTreeUpdateOperation(const ui::AXNode* node, + unsigned char byte); + void AddRoleSpecificProperties(FuzzerData& fuzz_data, + ui::AXNodeData& node, + const std::string& parentName, + size_t extra_data_size); + ax::mojom::Role GetInterestingRole(unsigned char byte, + ax::mojom::Role parent_role); + bool CanHaveChildren(ax::mojom::Role role); + bool CanHaveText(ax::mojom::Role role); + std::u16string GenerateInterestingText(const unsigned char* data, + size_t size); + ui::AXNodeID max_assigned_node_id_; + ui::TestAXTreeManager tree_manager_; +}; +#endif // UI_ACCESSIBILITY_AX_TREE_FUZZER_UTIL_H_ diff --git a/chromium/ui/accessibility/ax_tree_manager.cc b/chromium/ui/accessibility/ax_tree_manager.cc new file mode 100644 index 00000000000..311c23991c5 --- /dev/null +++ b/chromium/ui/accessibility/ax_tree_manager.cc @@ -0,0 +1,107 @@ +// Copyright 2022 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 "ui/accessibility/ax_tree_manager.h" + +#include "base/no_destructor.h" +#include "ui/accessibility/ax_export.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree_manager_map.h" +#include "ui/accessibility/ax_tree_observer.h" + +namespace ui { + +// static +AXTreeManagerMap& AXTreeManager::GetMap() { + static base::NoDestructor<AXTreeManagerMap> map; + return *map; +} + +// static +AXTreeManager* AXTreeManager::FromID(AXTreeID ax_tree_id) { + return ax_tree_id != AXTreeIDUnknown() ? GetMap().GetManager(ax_tree_id) + : nullptr; +} + +// static +AXTreeManager* AXTreeManager::ForChildTree(const AXNode& parent_node) { + if (!parent_node.HasStringAttribute( + ax::mojom::StringAttribute::kChildTreeId)) { + return nullptr; + } + + AXTreeID child_tree_id = AXTreeID::FromString( + parent_node.GetStringAttribute(ax::mojom::StringAttribute::kChildTreeId)); + AXTreeManager* child_tree_manager = GetMap().GetManager(child_tree_id); + + // Some platforms do not use AXTreeManagers, so child trees don't exist in + // the browser process. + DCHECK(!child_tree_manager || + !child_tree_manager->GetParentNodeFromParentTreeAsAXNode() || + child_tree_manager->GetParentNodeFromParentTreeAsAXNode()->id() == + parent_node.id()); + return child_tree_manager; +} + +AXTreeManager::AXTreeManager() + : ax_tree_id_(AXTreeIDUnknown()), + ax_tree_(nullptr), + event_generator_(ax_tree()) {} + +AXTreeManager::AXTreeManager(std::unique_ptr<AXTree> tree) + : ax_tree_id_(tree ? tree->data().tree_id : AXTreeIDUnknown()), + ax_tree_(std::move(tree)), + event_generator_(ax_tree()) { + GetMap().AddTreeManager(ax_tree_id_, this); +} + +AXTreeManager::AXTreeManager(const AXTreeID& tree_id, + std::unique_ptr<AXTree> tree) + : ax_tree_id_(tree_id), + ax_tree_(std::move(tree)), + event_generator_(ax_tree()) { + GetMap().AddTreeManager(ax_tree_id_, this); + if (ax_tree()) + tree_observation_.Observe(ax_tree()); +} + +AXTreeID AXTreeManager::GetTreeID() const { + return ax_tree_ ? ax_tree_->data().tree_id : AXTreeIDUnknown(); +} + +AXTreeID AXTreeManager::GetParentTreeID() const { + return ax_tree_ ? ax_tree_->data().parent_tree_id : AXTreeIDUnknown(); +} + +AXNode* AXTreeManager::GetRootAsAXNode() const { + return ax_tree_ ? ax_tree_->root() : nullptr; +} + +void AXTreeManager::WillBeRemovedFromMap() { + if (!ax_tree_) + return; + ax_tree_->NotifyTreeManagerWillBeRemoved(ax_tree_id_); +} + +AXTreeManager::~AXTreeManager() { + // Stop observing so we don't get a callback for every node being deleted. + event_generator_.ReleaseTree(); + if (ax_tree_) + GetMap().RemoveTreeManager(ax_tree_id_); +} + +void AXTreeManager::OnTreeDataChanged(AXTree* tree, + const AXTreeData& old_data, + const AXTreeData& new_data) { + GetMap().RemoveTreeManager(ax_tree_id_); + ax_tree_id_ = new_data.tree_id; + GetMap().AddTreeManager(ax_tree_id_, this); +} + +void AXTreeManager::RemoveFromMap() { + GetMap().RemoveTreeManager(ax_tree_id_); +} + +} // namespace ui diff --git a/chromium/ui/accessibility/ax_tree_manager.h b/chromium/ui/accessibility/ax_tree_manager.h index 52f892c1acf..df43f35a0c2 100644 --- a/chromium/ui/accessibility/ax_tree_manager.h +++ b/chromium/ui/accessibility/ax_tree_manager.h @@ -5,18 +5,32 @@ #ifndef UI_ACCESSIBILITY_AX_TREE_MANAGER_H_ #define UI_ACCESSIBILITY_AX_TREE_MANAGER_H_ +#include "base/scoped_observation.h" +#include "ui/accessibility/ax_event_generator.h" #include "ui/accessibility/ax_export.h" -#include "ui/accessibility/ax_node.h" -#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree.h" #include "ui/accessibility/ax_tree_observer.h" namespace ui { +class AXNode; +class AXTreeManagerMap; + // Abstract interface for a class that owns an AXTree and manages its // connections to other AXTrees in the same page or desktop (parent and child // trees). -class AX_EXPORT AXTreeManager { +class AX_EXPORT AXTreeManager : public AXTreeObserver { public: + static AXTreeManager* FromID(AXTreeID ax_tree_id); + // If the child of `parent_node` exists in a separate child tree, return the + // tree manager for that child tree. Otherwise, return nullptr. + static AXTreeManager* ForChildTree(const AXNode& parent_node); + + AXTreeManager(const AXTreeManager&) = delete; + AXTreeManager& operator=(const AXTreeManager&) = delete; + + ~AXTreeManager() override; + // Returns the AXNode with the given |node_id| from the tree that has the // given |tree_id|. This allows for callers to access nodes outside of their // own tree. Returns nullptr if |tree_id| or |node_id| is not found. @@ -28,20 +42,15 @@ class AX_EXPORT AXTreeManager { // Returns nullptr if |node_id| is not found. virtual AXNode* GetNodeFromTree(const AXNodeID node_id) const = 0; - // Use `AddObserver` and `RemoveObserver` when you want to be notified when - // changes happen to an `XTree` - virtual void AddObserver(AXTreeObserver* observer) {} - virtual void RemoveObserver(AXTreeObserver* observer) {} - // Returns the tree id of the tree managed by this AXTreeManager. - virtual AXTreeID GetTreeID() const = 0; + AXTreeID GetTreeID() const; // Returns the tree id of the parent tree. // Returns AXTreeIDUnknown if this tree doesn't have a parent tree. - virtual AXTreeID GetParentTreeID() const = 0; + virtual AXTreeID GetParentTreeID() const; // Returns the AXNode that is at the root of the current tree. - virtual AXNode* GetRootAsAXNode() const = 0; + AXNode* GetRootAsAXNode() const; // If this tree has a parent tree, returns the node in the parent tree that // hosts the current tree. Returns nullptr if this tree doesn't have a parent @@ -50,12 +59,61 @@ class AX_EXPORT AXTreeManager { // Called when the tree manager is about to be removed from the tree map, // `AXTreeManagerMap`. - virtual void WillBeRemovedFromMap() {} + void WillBeRemovedFromMap(); + + const AXTreeID& ax_tree_id() const { return ax_tree_id_; } + AXTree* ax_tree() const { return ax_tree_.get(); } + + const AXEventGenerator& event_generator() const { return event_generator_; } + AXEventGenerator& event_generator() { return event_generator_; } + + // AXTreeObserver implementation. + void OnTreeDataChanged(ui::AXTree* tree, + const ui::AXTreeData& old_data, + const ui::AXTreeData& new_data) override; + void OnNodeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override {} + void OnSubtreeWillBeDeleted(ui::AXTree* tree, ui::AXNode* node) override {} + void OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) override {} + void OnNodeDeleted(ui::AXTree* tree, int32_t node_id) override {} + void OnNodeReparented(ui::AXTree* tree, ui::AXNode* node) override {} + void OnRoleChanged(ui::AXTree* tree, + ui::AXNode* node, + ax::mojom::Role old_role, + ax::mojom::Role new_role) override {} + void OnAtomicUpdateFinished( + ui::AXTree* tree, + bool root_changed, + const std::vector<ui::AXTreeObserver::Change>& changes) override {} + + protected: + AXTreeManager(); + explicit AXTreeManager(std::unique_ptr<AXTree> tree); + explicit AXTreeManager(const AXTreeID& tree_id, std::unique_ptr<AXTree> tree); + + // TODO(benjamin.beaudry): Remove this helper once we move the logic related + // to the parent connection from `BrowserAccessibilityManager` to this class. + // `BrowserAccessibilityManager` needs to remove the manager from the map + // before calling `BrowserAccessibilityManager::ParentConnectionChanged`, so + // the default removal of the manager in `~AXTreeManager` occurs too late. + void RemoveFromMap(); + + AXTreeID ax_tree_id_; + std::unique_ptr<AXTree> ax_tree_; + + AXEventGenerator event_generator_; + + private: + friend class TestAXTreeManager; + + static AXTreeManagerMap& GetMap(); - // For debugging. - // TODO(benjamin.beaudry) Instead of this, implement GetTreeData() on all - // AXTreeManager subclasses, and have callers use GetTreeData().ToString(); - virtual std::string ToString() const = 0; + // Automatically stops observing notifications from the AXTree when this class + // is destructed. + // + // This member needs to be destructed before any observed AXTrees. Since + // destructors for non-static member fields are called in the reverse order of + // declaration, do not move this member above other members. + base::ScopedObservation<AXTree, AXTreeObserver> tree_observation_{this}; }; } // namespace ui diff --git a/chromium/ui/accessibility/ax_tree_manager_base.cc b/chromium/ui/accessibility/ax_tree_manager_base.cc index 5b452d85e5f..461ad3aaffd 100644 --- a/chromium/ui/accessibility/ax_tree_manager_base.cc +++ b/chromium/ui/accessibility/ax_tree_manager_base.cc @@ -25,10 +25,9 @@ AXTreeManagerBase* AXTreeManagerBase::GetManager(const AXTreeID& tree_id) { } // static -std::unordered_map<AXTreeID, AXTreeManagerBase*, AXTreeIDHash>& +base::flat_map<AXTreeID, AXTreeManagerBase*>& AXTreeManagerBase::GetTreeManagerMapInstance() { - static base::NoDestructor< - std::unordered_map<AXTreeID, AXTreeManagerBase*, AXTreeIDHash>> + static base::NoDestructor<base::flat_map<AXTreeID, AXTreeManagerBase*>> map_instance; return *map_instance; } diff --git a/chromium/ui/accessibility/ax_tree_manager_base.h b/chromium/ui/accessibility/ax_tree_manager_base.h index b42429d79e0..ac733dbaabb 100644 --- a/chromium/ui/accessibility/ax_tree_manager_base.h +++ b/chromium/ui/accessibility/ax_tree_manager_base.h @@ -6,8 +6,8 @@ #define UI_ACCESSIBILITY_AX_TREE_MANAGER_BASE_H_ #include <memory> -#include <unordered_map> +#include "base/containers/flat_map.h" #include "third_party/abseil-cpp/absl/types/optional.h" #include "ui/accessibility/ax_export.h" #include "ui/accessibility/ax_node_data.h" @@ -122,7 +122,7 @@ class AX_EXPORT AXTreeManagerBase final { AXTreeManagerBase* DetachChildTree(AXNode& host_node); private: - static std::unordered_map<AXTreeID, AXTreeManagerBase*, AXTreeIDHash>& + static base::flat_map<AXTreeID, AXTreeManagerBase*>& GetTreeManagerMapInstance(); std::unique_ptr<AXTree> tree_; diff --git a/chromium/ui/accessibility/ax_tree_manager_map.cc b/chromium/ui/accessibility/ax_tree_manager_map.cc index 8e12b1936b2..574f12cf3e8 100644 --- a/chromium/ui/accessibility/ax_tree_manager_map.cc +++ b/chromium/ui/accessibility/ax_tree_manager_map.cc @@ -5,8 +5,6 @@ #include "ui/accessibility/ax_tree_manager_map.h" #include "base/containers/contains.h" -#include "base/no_destructor.h" -#include "ui/accessibility/ax_enums.mojom.h" namespace ui { @@ -14,26 +12,20 @@ AXTreeManagerMap::AXTreeManagerMap() = default; AXTreeManagerMap::~AXTreeManagerMap() = default; -// static -AXTreeManagerMap& AXTreeManagerMap::GetInstance() { - static base::NoDestructor<AXTreeManagerMap> instance; - return *instance; -} - -void AXTreeManagerMap::AddTreeManager(AXTreeID tree_id, +void AXTreeManagerMap::AddTreeManager(const AXTreeID& tree_id, AXTreeManager* manager) { if (tree_id != AXTreeIDUnknown()) map_[tree_id] = manager; } -void AXTreeManagerMap::RemoveTreeManager(AXTreeID tree_id) { +void AXTreeManagerMap::RemoveTreeManager(const AXTreeID& tree_id) { if (auto* manager = GetManager(tree_id)) { manager->WillBeRemovedFromMap(); map_.erase(tree_id); } } -AXTreeManager* AXTreeManagerMap::GetManager(AXTreeID tree_id) { +AXTreeManager* AXTreeManagerMap::GetManager(const AXTreeID& tree_id) { if (tree_id == AXTreeIDUnknown()) return nullptr; auto iter = map_.find(tree_id); @@ -43,25 +35,4 @@ AXTreeManager* AXTreeManagerMap::GetManager(AXTreeID tree_id) { return iter->second; } -AXTreeManager* AXTreeManagerMap::GetManagerForChildTree( - const AXNode& parent_node) { - if (!parent_node.HasStringAttribute( - ax::mojom::StringAttribute::kChildTreeId)) { - return nullptr; - } - - AXTreeID child_tree_id = AXTreeID::FromString( - parent_node.GetStringAttribute(ax::mojom::StringAttribute::kChildTreeId)); - AXTreeManager* child_tree_manager = - AXTreeManagerMap::GetInstance().GetManager(child_tree_id); - - // Some platforms do not use AXTreeManagers, so child trees don't exist in - // the browser process. - DCHECK(!child_tree_manager || - !child_tree_manager->GetParentNodeFromParentTreeAsAXNode() || - child_tree_manager->GetParentNodeFromParentTreeAsAXNode()->id() == - parent_node.id()); - return child_tree_manager; -} - } // namespace ui diff --git a/chromium/ui/accessibility/ax_tree_manager_map.h b/chromium/ui/accessibility/ax_tree_manager_map.h index aff21f0be99..4a444e920b3 100644 --- a/chromium/ui/accessibility/ax_tree_manager_map.h +++ b/chromium/ui/accessibility/ax_tree_manager_map.h @@ -5,17 +5,16 @@ #ifndef UI_ACCESSIBILITY_AX_TREE_MANAGER_MAP_H_ #define UI_ACCESSIBILITY_AX_TREE_MANAGER_MAP_H_ -#include <unordered_map> - +#include "base/containers/flat_map.h" #include "ui/accessibility/ax_tree_id.h" #include "ui/accessibility/ax_tree_manager.h" namespace ui { -// This class manages AXTreeManager instances. It is a singleton wrapper -// around a std::unordered_map. AXTreeID's are used as the key for the map. -// Since AXTreeID's might refer to AXTreeIDUnknown, callers should not expect -// AXTreeIDUnknown to map to a particular AXTreeManager. +// This class manages AXTreeManager instances. It is a wrapper around a +// base::flat_map. AXTreeID's are used as the key for the map. Since AXTreeID's +// might refer to AXTreeIDUnknown, callers should not expect AXTreeIDUnknown to +// map to a particular AXTreeManager. class AX_EXPORT AXTreeManagerMap { public: AXTreeManagerMap(); @@ -23,17 +22,12 @@ class AX_EXPORT AXTreeManagerMap { AXTreeManagerMap(const AXTreeManagerMap& map) = delete; AXTreeManagerMap& operator=(const AXTreeManagerMap& map) = delete; - static AXTreeManagerMap& GetInstance(); - void AddTreeManager(AXTreeID tree_id, AXTreeManager* manager); - void RemoveTreeManager(AXTreeID tree_id); - AXTreeManager* GetManager(AXTreeID tree_id); - - // If the child of `parent_node` exists in a separate child tree, return the - // tree manager for that child tree. Otherwise, return nullptr. - AXTreeManager* GetManagerForChildTree(const AXNode& parent_node); + void AddTreeManager(const AXTreeID& tree_id, AXTreeManager* manager); + void RemoveTreeManager(const AXTreeID& tree_id); + AXTreeManager* GetManager(const AXTreeID& tree_id); private: - std::unordered_map<AXTreeID, AXTreeManager*, AXTreeIDHash> map_; + base::flat_map<AXTreeID, AXTreeManager*> map_; }; } // namespace ui diff --git a/chromium/ui/accessibility/ax_tree_serializer.h b/chromium/ui/accessibility/ax_tree_serializer.h index 4d97ae45f98..9d927ec695d 100644 --- a/chromium/ui/accessibility/ax_tree_serializer.h +++ b/chromium/ui/accessibility/ax_tree_serializer.h @@ -13,7 +13,6 @@ #include <memory> #include <ostream> #include <set> -#include <unordered_set> #include <vector> #include "base/debug/crash_logging.h" @@ -322,16 +321,14 @@ AXSourceNode AXTreeSerializer<AXSourceNode>::LeastCommonAncestor( // client ancestor chain disagree. The last node before they disagree // is the LCA. AXSourceNode lca = tree_->GetNull(); - int source_index = static_cast<int>(ancestors.size() - 1); - int client_index = static_cast<int>(client_ancestors.size() - 1); - while (source_index >= 0 && client_index >= 0) { - if (tree_->GetId(ancestors[source_index]) != - client_ancestors[client_index]->id) { + for (size_t source_index = ancestors.size(), + client_index = client_ancestors.size(); + source_index > 0 && client_index > 0; --source_index, --client_index) { + if (tree_->GetId(ancestors[source_index - 1]) != + client_ancestors[client_index - 1]->id) { return lca; } - lca = ancestors[source_index]; - source_index--; - client_index--; + lca = ancestors[source_index - 1]; } return lca; } diff --git a/chromium/ui/accessibility/ax_tree_unittest.cc b/chromium/ui/accessibility/ax_tree_unittest.cc index 9388778e097..97dbe0a005b 100644 --- a/chromium/ui/accessibility/ax_tree_unittest.cc +++ b/chromium/ui/accessibility/ax_tree_unittest.cc @@ -449,6 +449,10 @@ TEST(AXTreeTest, LeaveOrphanedDeletedSubtreeFails) { update.node_id_to_clear = 2; update.nodes.resize(1); update.nodes[0].id = 3; +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED(tree.Unserialize(update), + "Nodes left pending by the update: 2"); +#else EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Nodes left pending by the update: 2", tree.error()); histogram_tester.ExpectUniqueSample( @@ -456,6 +460,7 @@ TEST(AXTreeTest, LeaveOrphanedDeletedSubtreeFails) { AXTreeUnserializeError::kPendingNodes, 1); histogram_tester.ExpectTotalCount( "Accessibility.Performance.Tree.Unserialize", 2); +#endif } TEST(AXTreeTest, LeaveOrphanedNewChildFails) { @@ -475,6 +480,10 @@ TEST(AXTreeTest, LeaveOrphanedNewChildFails) { update.nodes.resize(1); update.nodes[0].id = 1; update.nodes[0].child_ids.push_back(2); +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED(tree.Unserialize(update), + "Nodes left pending by the update: 2"); +#else EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Nodes left pending by the update: 2", tree.error()); histogram_tester.ExpectUniqueSample( @@ -482,6 +491,7 @@ TEST(AXTreeTest, LeaveOrphanedNewChildFails) { AXTreeUnserializeError::kPendingNodes, 1); histogram_tester.ExpectTotalCount( "Accessibility.Performance.Tree.Unserialize", 2); +#endif } TEST(AXTreeTest, DuplicateChildIdFails) { @@ -502,6 +512,10 @@ TEST(AXTreeTest, DuplicateChildIdFails) { update.nodes[0].child_ids.push_back(2); update.nodes[0].child_ids.push_back(2); update.nodes[1].id = 2; +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED(tree.Unserialize(update), + "Node 1 has duplicate child id 2"); +#else EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Node 1 has duplicate child id 2", tree.error()); histogram_tester.ExpectUniqueSample( @@ -509,6 +523,7 @@ TEST(AXTreeTest, DuplicateChildIdFails) { AXTreeUnserializeError::kDuplicateChild, 1); histogram_tester.ExpectTotalCount( "Accessibility.Performance.Tree.Unserialize", 1); +#endif } TEST(AXTreeTest, InvalidReparentingFails) { @@ -536,6 +551,11 @@ TEST(AXTreeTest, InvalidReparentingFails) { update.nodes[0].child_ids.push_back(2); update.nodes[1].id = 2; update.nodes[2].id = 3; +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED( + tree.Unserialize(update), + "Node 3 is not marked for destruction, would be reparented to 1"); +#else EXPECT_FALSE(tree.Unserialize(update)); ASSERT_EQ("Node 3 is not marked for destruction, would be reparented to 1", tree.error()); @@ -544,6 +564,7 @@ TEST(AXTreeTest, InvalidReparentingFails) { AXTreeUnserializeError::kReparent, 1); histogram_tester.ExpectTotalCount( "Accessibility.Performance.Tree.Unserialize", 1); +#endif } TEST(AXTreeTest, NoReparentingOfRootIfNoNewRoot) { @@ -954,6 +975,7 @@ TEST(AXTreeTest, ImplicitAttributeDelete) { initial_state.root_id = 1; initial_state.nodes.resize(1); initial_state.nodes[0].id = 1; + initial_state.nodes[0].role = ax::mojom::Role::kGroup; initial_state.nodes[0].SetName("Node 1 name"); AXTree tree(initial_state); @@ -1205,7 +1227,13 @@ TEST(AXTreeTest, BogusAXTree2) { node2.child_ids.push_back(0); initial_state.nodes.push_back(node2); ui::AXTree tree; - tree.Unserialize(initial_state); +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED(tree.Unserialize(initial_state), + "Node 0 has duplicate child id 0"); +#else + EXPECT_FALSE(tree.Unserialize(initial_state)); + EXPECT_EQ("Node 0 has duplicate child id 0", tree.error()); +#endif } // UAF caught by ax_tree_fuzzer @@ -1223,7 +1251,13 @@ TEST(AXTreeTest, BogusAXTree3) { initial_state.nodes.push_back(node2); ui::AXTree tree; - tree.Unserialize(initial_state); +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED(tree.Unserialize(initial_state), + "Node 1 has duplicate child id 1"); +#else + EXPECT_FALSE(tree.Unserialize(initial_state)); + EXPECT_EQ("Node 1 has duplicate child id 1", tree.error()); +#endif } TEST(AXTreeTest, RoleAndStateChangeCallbacks) { @@ -5381,10 +5415,17 @@ TEST(AXTreeTest, UnserializeErrors) { ui::AXNodeData disconnected_node; disconnected_node.id = 2; tree_update_3.nodes.push_back(disconnected_node); +#if defined(AX_FAIL_FAST_BUILD) + EXPECT_DEATH_IF_SUPPORTED( + tree.Unserialize(tree_update_3), + "2 will not be in the tree and is not the new root"); +#else EXPECT_FALSE(tree.Unserialize(tree_update_3)); + EXPECT_EQ("2 will not be in the tree and is not the new root", tree.error()); histogram_tester.ExpectUniqueSample( "Accessibility.Reliability.Tree.UnserializeError", AXTreeUnserializeError::kNotInTree, 1); +#endif } } // namespace ui diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/background.js b/chromium/ui/accessibility/extensions/color_contrast_companion/background.js new file mode 100644 index 00000000000..68e985c2c6c --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/background.js @@ -0,0 +1,96 @@ +// Copyright 2022 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. + +var stream = null; +var ui = null; + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + console.log('Got message'); + console.log(request); + if (request.close && ui) { + chrome.windows.remove(ui.id); + } +}); + +function capture() { + let video = document.createElement('video'); + video.autoplay = true; + document.body.appendChild(video); + video.addEventListener('canplay', () => { + if (video.videoWidth < 100) { + // We probably need the permission again. + getStream(); + return; + } + + window.setTimeout(() => { + let canvas = document.createElement('canvas'); + canvas.height = video.videoHeight; + canvas.width = video.videoWidth; + var context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, canvas.width, canvas.height); + var imageDataUrl = canvas.toDataURL(); + document.body.removeChild(video); + + // Close the stream so it stops using resources. + let tracks = stream.getTracks(); + tracks.forEach(function(track) { + track.stop(); + }); + stream = null; + + chrome.windows.create( + { + 'url': chrome.runtime.getURL('ui.html'), + 'focused': true, + 'type': 'popup', + 'state': 'fullscreen' + }, + (win) => { + ui = win; + var tab = win.tabs[0]; + window.setTimeout(() => { + console.log('Sending message'); + chrome.tabs.sendMessage(tab.id, {'imageDataUrl': imageDataUrl}); + }, 250); + }); + }, 250); + }); + video.srcObject = stream; +} + +function getStream() { + chrome.desktopCapture.chooseDesktopMedia(['screen'], (streamId) => { + let video = document.createElement('video'); + navigator.mediaDevices + .getUserMedia({ + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: streamId, + } + } + }) + .then(returnedStream => { + stream = returnedStream; + capture(); + }); + }); +} + +chrome.browserAction.onClicked.addListener(() => { + if (!stream) { + getStream(); + return; + } + + capture(); +}); + +var alreadyShowedHelp = localStorage.getItem('help'); +if (!alreadyShowedHelp) { + localStorage.setItem('help', 'true'); + chrome.windows.create( + {'url': chrome.runtime.getURL('help.html'), 'focused': true}); +} diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/browser_action.png b/chromium/ui/accessibility/extensions/color_contrast_companion/browser_action.png Binary files differnew file mode 100644 index 00000000000..a72899c2832 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/browser_action.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/common.js b/chromium/ui/accessibility/extensions/color_contrast_companion/common.js new file mode 100644 index 00000000000..b4cf068274b --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/common.js @@ -0,0 +1,101 @@ +// Copyright (c) 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. + +var DEFAULT_SCHEME = 3; +var MAX_SCHEME = 5; + +function $(id) { + return document.getElementById(id); +} + +function getEnabled() { + var result = localStorage['enabled']; + if (result === 'true' || result === 'false') { + return (result === 'true'); + } + localStorage['enabled'] = 'true'; + return true; +} + +function setEnabled(enabled) { + localStorage['enabled'] = enabled; +} + +function getKeyAction() { + var keyAction = localStorage['keyaction']; + if (keyAction == 'global' || keyAction == 'site') { + return keyAction; + } + keyAction = 'global'; + localStorage['keyaction'] = keyAction; + return keyAction; +} + +function setKeyAction(keyAction) { + if (keyAction != 'global' && keyAction != 'site') { + keyAction = 'global'; + } + localStorage['keyaction'] = keyAction; +} + +function getDefaultScheme() { + var scheme = localStorage['scheme']; + if (scheme >= 0 && scheme <= MAX_SCHEME) { + return scheme; + } + scheme = DEFAULT_SCHEME; + localStorage['scheme'] = scheme; + return scheme; +} + +function setDefaultScheme(scheme) { + if (!(scheme >= 0 && scheme <= MAX_SCHEME)) { + scheme = DEFAULT_SCHEME; + } + localStorage['scheme'] = scheme; +} + +function getSiteScheme(site) { + var scheme = getDefaultScheme(); + try { + var siteSchemes = JSON.parse(localStorage['siteschemes']); + scheme = siteSchemes[site]; + if (!(scheme >= 0 && scheme <= MAX_SCHEME)) { + scheme = getDefaultScheme(); + } + } catch (e) { + scheme = getDefaultScheme(); + } + return scheme; +} + +function setSiteScheme(site, scheme) { + if (!(scheme >= 0 && scheme <= MAX_SCHEME)) { + scheme = getDefaultScheme(); + } + var siteSchemes = {}; + try { + siteSchemes = JSON.parse(localStorage['siteschemes']); + siteSchemes['www.example.com'] = getDefaultScheme(); + } catch (e) { + siteSchemes = {}; + } + siteSchemes[site] = scheme; + localStorage['siteschemes'] = JSON.stringify(siteSchemes); +} + +function resetSiteSchemes() { + var siteSchemes = {}; + localStorage['siteschemes'] = JSON.stringify(siteSchemes); +} + +function siteFromUrl(url) { + var a = document.createElement('a'); + a.href = url; + return a.hostname; +} + +function isDisallowedUrl(url) { + return url.startsWith('chrome') || url.startsWith('about'); +} diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-128.png b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-128.png Binary files differnew file mode 100644 index 00000000000..2814544cafb --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-128.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-16.png b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-16.png Binary files differnew file mode 100644 index 00000000000..9313a62fc67 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-16.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-19.png b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-19.png Binary files differnew file mode 100644 index 00000000000..84d1b8271dd --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-19.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-38.png b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-38.png Binary files differnew file mode 100644 index 00000000000..998b3f1a1dc --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-38.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-48.png b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-48.png Binary files differnew file mode 100644 index 00000000000..0f61571e41e --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/contrast-48.png diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/help.html b/chromium/ui/accessibility/extensions/color_contrast_companion/help.html new file mode 100644 index 00000000000..e99eb01c612 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/help.html @@ -0,0 +1,151 @@ +<head> + <title> + Color Contrast Companion + </title> + <style> + .logo { + float: left; + } + h1 { + padding-top: 24px; + } + body, div, p { + font-family: system-ui, sans-serif; + font-size: 14pt; + line-height: 1.5; + } + p { + margin: 24px 0; + } + body { + background-color: #eee; + padding: 0; + margin: 0; + } + #main { + background-color: #fff; + width: 780px; + margin: 0 auto; + border-left: 1px solid #444; + border-right: 1px solid #444; + padding: 24px; + min-height: 500px; + } + .top { + width: 100%; + float: left; + } + .note { + padding: 0 16px; + border: 1px solid #88e; + } + </style> +</head> +<body> + <div id="main"> + <div class="top"> + <div class="logo"> + <img src="contrast-128.png" alt="Color Contrast Companion logo"> + </div> + <h1>Color Contrast Companion</h2> + <p> + <i>Quickly compute the color contrast of pixels anywhere on your screen.</i> + </p> + </div> + + <p> + This is a tool to compute the contrast between a foreground and + background color, to help test whether an application is + providing enough contrast so that text can be read by people + with moderately low vision. + </p> + + <p> + <a href="https://www.w3.org/TR/WCAG21/#contrast-minimum">WCAG</a> + recommends these contrast ratios: + <ul> + <li> AA: 4.5:1 for all text, 3:1 for 18 pts and larger. + <li> AAA: 7:1 for all text, 4.5:1 for 18 pts and larger. + <li> Focus indicators: 3:1 if 3px thick, 4.5:1 if less. + <a href="https://www.w3.org/WAI/GL/low-vision-a11y-tf/wiki/Contrast_(Minimum)#Focus_Indicators">(Source)</a> + </ul> + </p> + <p> + For more information, see the links at the bottom of this page. + </p> + + <h2>How to use Color Contrast Companion</h2> + + <div class="note"> + <p> + <b>Note</b>: + If you're a web developer or if you're testing a web app, you already + have great color contrast tools built directly into Chrome's + Developer Tools! For more information, see + <a href="https://developers.google.com/web/updates/2018/01/devtools#contrast"> + Contrast ratio in the Color Picker</a>. + </p> + <p> + However, there are cases where you might want to check the contrast of + something on your screen that you can't inspect in Chrome's Developer Tools - + such as Chrome's UI, text inside an image, or another app outside of Chrome. + For those cases, this extension can help! + </p> + </div> + + <p> + <b>Step 1</b>: + Click on the Color Contrast Companion icon on the right side of the + Chrome toolbar. If you have a lot of extensions installed, the icon + might be inside the Chrome menu (the three dots). + </p> + + <p> + <img src="browser_action.png" alt="Image of icon in Chrome toolbar"> + </p> + + <p> + <b>Step 2</b>: + A <b>Share Your Screen</b> dialog pops up asking for your permission to + share your entire screen with Color Contrast Companion. + This is necessary for Color Contrast Companion to take a screenshot of + your entire desktop. This image is never saved or sent to any server, it's + only used to let you pick colors. Click the Share button. + </p> + + <p> + <b>Step 3</b>: + A window opens up showing a screenshot of your computer screen, magnified. + Scroll to the portion of the screen containing the pixels you're interested + in. Click once to pick the foreground color, click again to pick the + background color. Keep clicking as many times as necessary if you're not + happy with the colors you picked the first time. + </p> + + <p> + <b>Step 4</b>: + The contrast ratio is shown at the top of the page. Copy the text from the + text box and paste it directly into a bug report if necessary, then click + the Close button. + </p> + + <h2>Further reading</h2> + + <p> + <ul> + <li> + <a href="https://www.w3.org/TR/WCAG21/#contrast-minimum"> + Web Contents Accessibility Guidelines (WCAG) 2.1 spec + </a> + <li> + + <a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"> + Understanding Success Criterion 1.4.3: Contrast (Minimum) + </a> + </li> + </ul> + </p> + + </div> + +</body> diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/highcontrast.js b/chromium/ui/accessibility/extensions/color_contrast_companion/highcontrast.js new file mode 100644 index 00000000000..83e31cfee98 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/highcontrast.js @@ -0,0 +1,177 @@ +// Copyright (c) 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. + +var mode; +var enabled = false; +var scheme = ''; +var timeoutId = null; + +var filterMap = { + '0': 'url("#hc_extension_off")', + '1': 'url("#hc_extension_highcontrast")', + '2': 'url("#hc_extension_grayscale")', + '3': 'url("#hc_extension_invert")', + '4': 'url("#hc_extension_invert_grayscale")', + '5': 'url("#hc_extension_yellow_on_black")' +}; + +var svgContent = + '<svg xmlns="http://www.w3.org/2000/svg" version="1.1"><defs><filter x="0" y="0" width="99999" height="99999" id="hc_extension_off"><feComponentTransfer><feFuncR type="table" tableValues="0 1"/><feFuncG type="table" tableValues="0 1"/><feFuncB type="table" tableValues="0 1"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_highcontrast"><feComponentTransfer><feFuncR type="gamma" exponent="3.0"/><feFuncG type="gamma" exponent="3.0"/><feFuncB type="gamma" exponent="3.0"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_highcontrast_back"><feComponentTransfer><feFuncR type="gamma" exponent="0.33"/><feFuncG type="gamma" exponent="0.33"/><feFuncB type="gamma" exponent="0.33"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_grayscale"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="gamma" exponent="3"/><feFuncG type="gamma" exponent="3"/><feFuncB type="gamma" exponent="3"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_grayscale_back"><feComponentTransfer><feFuncR type="gamma" exponent="0.33"/><feFuncG type="gamma" exponent="0.33"/><feFuncB type="gamma" exponent="0.33"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_invert"><feComponentTransfer><feFuncR type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncG type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncB type="gamma" amplitude="-1" exponent="3" offset="1"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_invert_back"><feComponentTransfer><feFuncR type="table" tableValues="1 0"/><feFuncG type="table" tableValues="1 0"/><feFuncB type="table" tableValues="1 0"/></feComponentTransfer><feComponentTransfer><feFuncR type="gamma" exponent="1.7"/><feFuncG type="gamma" exponent="1.7"/><feFuncB type="gamma" exponent="1.7"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_invert_grayscale"><feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/><feComponentTransfer><feFuncR type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncG type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncB type="gamma" amplitude="-1" exponent="3" offset="1"/></feComponentTransfer></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_yellow_on_black"><feComponentTransfer><feFuncR type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncG type="gamma" amplitude="-1" exponent="3" offset="1"/><feFuncB type="gamma" amplitude="-1" exponent="3" offset="1"/></feComponentTransfer><feColorMatrix type="matrix" values="0.3 0.5 0.2 0 0 0.3 0.5 0.2 0 0 0 0 0 0 0 0 0 0 1 0"/></filter><filter x="0" y="0" width="99999" height="99999" id="hc_extension_yellow_on_black_back"><feComponentTransfer><feFuncR type="table" tableValues="1 0"/><feFuncG type="table" tableValues="1 0"/><feFuncB type="table" tableValues="1 0"/></feComponentTransfer><feComponentTransfer><feFuncR type="gamma" exponent="0.33"/><feFuncG type="gamma" exponent="0.33"/><feFuncB type="gamma" exponent="0.33"/></feComponentTransfer></filter></defs></svg>'; + +var cssTemplate = + 'html[hc="a0"] { -webkit-filter: url("#hc_extension_off"); } html[hcx="0"] img[src*="jpg"], html[hcx="0"] img[src*="jpeg"], html[hcx="0"] svg image, html[hcx="0"] img.rg_i, html[hcx="0"] embed, html[hcx="0"] object, html[hcx="0"] video { -webkit-filter: url("#hc_extension_off"); } html[hc="a1"] { -webkit-filter: url("#hc_extension_highcontrast"); } html[hcx="1"] img[src*="jpg"], html[hcx="1"] img[src*="jpeg"], html[hcx="1"] img.rg_i, html[hcx="1"] svg image, html[hcx="1"] embed, html[hcx="1"] object, html[hcx="1"] video { -webkit-filter: url("#hc_extension_highcontrast_back"); } html[hc="a2"] { -webkit-filter: url("#hc_extension_grayscale"); } html[hcx="2"] img[src*="jpg"], html[hcx="2"] img[src*="jpeg"], html[hcx="2"] img.rg_i, html[hcx="2"] svg image, html[hcx="2"] embed, html[hcx="2"] object, html[hcx="2"] video { -webkit-filter: url("#hc_extension_grayscale_back"); } html[hc="a3"] { -webkit-filter: url("#hc_extension_invert"); } html[hcx="3"] img[src*="jpg"], html[hcx="3"] img[src*="jpeg"], html[hcx="3"] img.rg_i, html[hcx="3"] svg image, html[hcx="3"] embed, html[hcx="3"] object, html[hcx="3"] video { -webkit-filter: url("#hc_extension_invert_back"); } html[hc="a4"] { -webkit-filter: url("#hc_extension_invert_grayscale"); } html[hcx="4"] img[src*="jpg"], html[hcx="4"] img[src*="jpeg"], html[hcx="4"] img.rg_i, html[hcx="4"] svg image, html[hcx="4"] embed, html[hcx="4"] object, html[hcx="4"] video { -webkit-filter: url("#hc_extension_invert_back"); } html[hc="a5"] { -webkit-filter: url("#hc_extension_yellow_on_black"); } html[hcx="5"] img[src*="jpg"], html[hcx="5"] img[src*="jpeg"], html[hcx="5"] img.rg_i, html[hcx="5"] svg image, html[hcx="5"] embed, html[hcx="5"] object, html[hcx="5"] video { -webkit-filter: url("#hc_extension_yellow_on_black_back"); }'; + +/** + * Add the elements to the pgae that make high-contrast adjustments possible. + */ +function addOrUpdateExtraElements() { + if (!enabled) + return; + + // We used to include the CSS, but that doesn't work when the document + // uses the <base> element to set a relative url. So instead we + // add a <style> element directly to the document with the right + // urls hard-coded into it. + var style = document.getElementById('hc_style'); + if (!style) { + var baseUrl = window.location.href.replace(window.location.hash, ''); + var css = cssTemplate.replace(/#/g, baseUrl + '#'); + style = document.createElement('style'); + style.id = 'hc_style'; + style.setAttribute('type', 'text/css'); + style.innerHTML = css; + document.head.appendChild(style); + } + + // Starting in Chrome 45 we can't apply a filter to the html element, + // so instead we create an element with low z-index that copies the + // body's background. + var bg = document.getElementById('hc_extension_bkgnd'); + if (!bg) { + bg = document.createElement('div'); + bg.id = 'hc_extension_bkgnd'; + bg.style.position = 'fixed'; + bg.style.left = '0px'; + bg.style.top = '0px'; + bg.style.right = '0px'; + bg.style.bottom = '0px'; + bg.style.zIndex = -1999999999; + document.body.appendChild(bg); + } + bg.style.display = 'block'; + bg.style.background = window.getComputedStyle(document.body).background; + + // As a special case, replace a zero-alpha background with white, + // otherwise we can't invert it. + var c = bg.style.backgroundColor; + c = c.replace(/\s\s*/g, ''); + if (m = /^rgba\(([\d]+),([\d]+),([\d]+),([\d]+|[\d]*.[\d]+)\)/.exec(c)) { + if (m[4] == '0') { + bg.style.backgroundColor = '#fff'; + } + } + + // Add a hidden element with the SVG filters. + var wrap = document.getElementById('hc_extension_svg_filters'); + if (wrap) + return; + + wrap = document.createElement('span'); + wrap.id = 'hc_extension_svg_filters'; + wrap.setAttribute('hidden', ''); + wrap.innerHTML = svgContent; + document.body.appendChild(wrap); +} + +/** + * This is called on load and every time the mode might have changed + * (i.e. enabling/disabling, or changing the type of contrast adjustment + * for this page). + */ +function update() { + var html = document.documentElement; + if (enabled) { + if (!document.body) { + window.setTimeout(update, 100); + return; + } + addOrUpdateExtraElements(); + if (html.getAttribute('hc') != mode + scheme) + html.setAttribute('hc', mode + scheme); + if (html.getAttribute('hcx') != scheme) + html.setAttribute('hcx', scheme); + + if (window == window.top) { + window.scrollBy(0, 1); + window.scrollBy(0, -1); + } + } else { + html.setAttribute('hc', mode + '0'); + html.setAttribute('hcx', '0'); + window.setTimeout(function() { + html.removeAttribute('hc'); + html.removeAttribute('hcx'); + var bg = document.getElementById('hc_extension_bkgnd'); + if (bg) + bg.style.display = 'none'; + }, 0); + } +} + +/** + * Called when we get a message from the background page. + */ +function onExtensionMessage(request) { + if (enabled != request.enabled || scheme != request.scheme) { + enabled = request.enabled; + scheme = request.scheme; + update(); + } +} + +/** + * KeyDown event handler + */ +function onKeyDown(evt) { + if (evt.keyCode == 122 /* F11 */ && evt.shiftKey) { + chrome.extension.sendRequest({'toggle_global': true}); + evt.stopPropagation(); + evt.preventDefault(); + return false; + } + if (evt.keyCode == 123 /* F12 */ && evt.shiftKey) { + chrome.extension.sendRequest({'toggle_site': true}); + evt.stopPropagation(); + evt.preventDefault(); + return false; + } + return true; +} + +function init() { + if (window == window.top) { + mode = 'a'; + } else { + mode = 'b'; + } + chrome.extension.onRequest.addListener(onExtensionMessage); + chrome.extension.sendRequest({'init': true}, onExtensionMessage); + document.addEventListener('keydown', onKeyDown, false); + + // Update again after a few seconds and again after load so that + // the background isn't wrong for long. + window.setTimeout(addOrUpdateExtraElements, 2000); + window.addEventListener('load', function() { + addOrUpdateExtraElements(); + + // Also update when the document body attributes change. + var config = {attributes: true, childList: false, characterData: false}; + var observer = new MutationObserver(function(mutations) { + addOrUpdateExtraElements(); + }); + observer.observe(document.body, config); + }); +} + +init(); diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/manifest.json b/chromium/ui/accessibility/extensions/color_contrast_companion/manifest.json new file mode 100644 index 00000000000..2f065558368 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/manifest.json @@ -0,0 +1,22 @@ +{ + "background": { + "scripts": [ "background.js" ] + }, + "browser_action": { + "default_icon": { + "19": "contrast-19.png", + "38": "contrast-38.png" + }, + "default_title": "Color Contrast Companion" + }, + "description": "Quickly compute the color contrast of pixels anywhere on your screen.", + "icons": { + "128": "contrast-128.png", + "16": "contrast-16.png", + "48": "contrast-48.png" + }, + "manifest_version": 2, + "name": "Color Contrast Companion", + "permissions": [ "desktopCapture", "tabs" ], + "version": "0.0.5" +} diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/ui.html b/chromium/ui/accessibility/extensions/color_contrast_companion/ui.html new file mode 100644 index 00000000000..fafc5183701 --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/ui.html @@ -0,0 +1,130 @@ +<head> + <style> + #top_panel { + position: fixed; + top: 0px; + left: 0px; + right: 0px; + height: 96px; + display: flex; + } + #img_panel { + cursor: crosshair; + position: fixed; + bottom: 0px; + left: 0px; + right: 0px; + top: 96px; + overflow: scroll; + padding: 4px; + border: 4px solid #ff9900; + } + .color { + margin: 8px; + width: 128px; + height: 48px; + border: 1px solid #000; + } + .rgb { + } + .ratio { + width: 88px; + height: 48px; + border: 1px solid #000; + text-align: center; + line-height: 48px; + font-size: 18px; + margin: 8px; + } + #details { + width: 148px; + height: 72px; + margin: 8px; + } + .current { + outline: 3px solid #f00; + } + .group { + } + .gtop { + } + .caption { + margin-left: 8px; + } + button { + width: 90px; + height: 32px; + margin: 8px 8px 0 8px; + color: #000; + font-weight: bold; + background-color: #fc9; + } + button:disabled { + color: #999; + } + canvas { + image-rendering: pixelated; + transform-origin: top left; + } + #highlight_container { + position: relative; + width: 0; + height: 0; + overflow: visible; + } + #highlight { + position: absolute; + outline: 2px solid #ff9900; + outline-offset: 2px; + z-index: 2; + pointer-events: none; + } + </style> +</head> +<body> + <div id="top_panel"> + <div style="width:96px"> + <img src="contrast-128.png" width=96 height=96 > + </div> + <div class="group"> + <div class="gtop"> + <div class="color" id="hover"></div> + </div> + <div class="caption">Hover: <span class="rgb" id="hoverrgb"></span></div> + </div> + <div class="group"> + <div class="gtop"> + <div class="color" id="fg"></div> + </div> + <div class="caption">Foreground: <span class="rgb" id="fgrgb"><span></div> + </div> + <div class="group"> + <div class="gtop"> + <div class="color" id="bg"></div> + </div> + <div class="caption">Background: <span class="rgb" id="bgrgb"></span></div> + </div> + <div> + <div class="ratio" id="ratio"></div> + <div class="caption">Contrast Ratio</div> + </div> + <div> + <textarea id="details"></textarea> + </div> + <div style="width: 104px"> + <button id="zoomin">Zoom In (+)</button> + <button id="zoomout">Zoom Out (-)</button> + </div> + <div style="width: 104px"> + <button id="close">Close</button> + <button id="help">Help</button> + </div> + </div> + <div id="img_panel"> + <div id="highlight_container"> + <div id="highlight"></div> + </div> + <canvas></canvas> + </div> + <script src="ui.js"></script> +</body> diff --git a/chromium/ui/accessibility/extensions/color_contrast_companion/ui.js b/chromium/ui/accessibility/extensions/color_contrast_companion/ui.js new file mode 100644 index 00000000000..4fc2ff3df9c --- /dev/null +++ b/chromium/ui/accessibility/extensions/color_contrast_companion/ui.js @@ -0,0 +1,290 @@ +// Copyright 2022 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. + +var maxScale = 32; + +var scale = 4; +var fgtoggle = true; +var fgcolor = null; +var bgcolor = null; +var srcImage = null; +var imageData = null; +var prevScale = 1; + +let close = document.getElementById('close'); +close.addEventListener('click', () => { + console.log('Sending message'); + chrome.runtime.sendMessage({'close': true}); +}); +let help = document.getElementById('help'); +help.addEventListener('click', () => { + window.open(chrome.runtime.getURL('help.html'), '_blank'); +}); + +let canvas = document.querySelector('canvas'); + +function repaint() { + console.log('Repaint ' + scale); + let width = srcImage.naturalWidth; + let height = srcImage.naturalHeight; + let context = canvas.getContext('2d'); + context.imageSmoothingEnabled = false; + canvas.style.transform = 'scale(' + scale + ')'; + + canvas.width = 1; + canvas.height = 1; + canvas.offsetLeft; + canvas.width = width; + canvas.height = height; + canvas.offsetLeft; + + context.drawImage(srcImage, 0, 0, width, height); + imageData = context.getImageData(0, 0, width, height).data; +} + + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.imageDataUrl) { + var img = document.createElement('img'); + img.addEventListener('load', () => { + srcImage = img; + repaint(); + }); + img.src = request.imageDataUrl; + } +}); + +let hover = document.getElementById('hover'); +let hoverrgb = document.getElementById('hoverrgb'); +let fg = document.getElementById('fg'); +let fgrgb = document.getElementById('fgrgb'); +let bg = document.getElementById('bg'); +let bgrgb = document.getElementById('bgrgb'); +let scrollpanel = document.getElementById('img_panel'); +let ratio = document.getElementById('ratio'); +let details = document.getElementById('details'); +let zoomin = document.getElementById('zoomin'); +let zoomout = document.getElementById('zoomout'); +let highlight = document.getElementById('highlight'); + +fg.classList.add('current'); +bg.classList.remove('current'); + +function rgbToHex(color) { + var r = color[0]; + var g = color[1]; + var b = color[2]; + return '#' + ((r << 16) | (g << 8) | b).toString(16); +} + +function getRelativeLuminance(color) { + var rSRGB = color[0] / 255; + var gSRGB = color[1] / 255; + var bSRGB = color[2] / 255; + var r = + rSRGB <= .03928 ? rSRGB / 12.92 : Math.pow((rSRGB + .055) / 1.055, 2.4); + var g = + gSRGB <= .03928 ? gSRGB / 12.92 : Math.pow((gSRGB + .055) / 1.055, 2.4); + var b = + bSRGB <= .03928 ? bSRGB / 12.92 : Math.pow((bSRGB + .055) / 1.055, 2.4); + return .2126 * r + .7152 * g + .0722 * b; +}; + +function getContrast(color1, color2) { + var c1lum = getRelativeLuminance(color1); + var c2lum = getRelativeLuminance(color2); + return (Math.max(c1lum, c2lum) + .05) / (Math.min(c1lum, c2lum) + .05); +} + +let context = canvas.getContext('2d'); +let bounds = canvas.getBoundingClientRect(); + +function getXY(evt) { + var x = evt.clientX + scrollpanel.scrollLeft - bounds.left; + var y = evt.clientY + scrollpanel.scrollTop - bounds.top; + return [Math.floor(x / scale), Math.floor(y / scale)]; +} + +function getColor(x, y) { + try { + var pixelIndex = y * srcImage.naturalWidth + x; + return imageData.slice(4 * pixelIndex, 4 * (pixelIndex + 1)); + } catch (e) { + return [0, 0, 0, 0]; + } +} + +function brightness(color) { + return (color[0] + color[1] + color[2]) / 3; +} + +function localMax(x, y) { + // This needs to be optional. Doesn't always do what we want. + return [x, y]; + + if (x < 0 || x >= srcImage.naturalWidth || y < 0 || + y + j >= srcImage.naturalHeight) { + return [x, y]; + } + + var ctr = getColor(x, y); + var max = brightness(ctr); + + var max; + var amax; + for (var i = -2; i <= 2; i++) { + for (var j = -2; j <= 2; j++) { + if (x + i < 0 || x + i >= srcImage.naturalWidth) + continue; + if (y + j < 0 || y + j >= srcImage.naturalHeight) + continue; + var c = getColor(x + i, y + j); + var cbright = brightness(c); + if (max > 128 && cbright > max) { + max = cbright; + amax = [x + i, y + j]; + } else if (max < 128 && cbright < max) { + max = cbright; + amax = [x + i, y + j]; + } + } + } + + if (amax) + return amax; + else + return [x, y]; +} + +canvas.addEventListener('mousemove', (evt) => { + var x1, y1, x, y; + [x1, y1] = getXY(evt); + [x, y] = localMax(x1, y1); + var color = getColor(x, y); + var hex = rgbToHex(color); + hover.style.backgroundColor = hex; + hoverrgb.innerText = hex; + + if (scale >= 8) { + highlight.style.display = 'block'; + highlight.style.left = (scale * x) + 'px'; + highlight.style.top = (scale * y) + 'px'; + highlight.style.width = scale + 'px'; + highlight.style.height = scale + 'px'; + highlight.style.top = (scale * y) + 'px'; + } else { + highlight.style.display = 'none'; + } +}); + +canvas.addEventListener('mouseenter', (evt) => { + highlight.style.display = 'block'; +}); + +canvas.addEventListener('mouseleave', (evt) => { + highlight.style.display = 'none'; +}); + +canvas.addEventListener('click', (evt) => { + var x1, y1, x, y; + [x1, y1] = getXY(evt); + [x, y] = localMax(x1, y1); + var color = getColor(x, y); + var hex = rgbToHex(color); + if (fgtoggle) { + fg.style.backgroundColor = hex; + fgrgb.innerText = hex; + fgcolor = color; + bg.classList.add('current'); + fg.classList.remove('current'); + } else { + bg.style.backgroundColor = hex; + bgrgb.innerText = hex; + bgcolor = color; + fg.classList.add('current'); + bg.classList.remove('current'); + } + fgtoggle = !fgtoggle; + if (fgcolor && bgcolor) { + var contrast = getContrast(fgcolor, bgcolor); + ratio.innerText = contrast.toFixed(2); + details.innerHTML = 'Foreground: ' + fgrgb.innerText + '\n' + + 'Background: ' + bgrgb.innerText + '\n' + + 'Ratio: ' + ratio.innerText; + details.select(); + } +}); + +function updateZoom() { + highlight.style.display = 'none'; + var prevScrollLeft = scrollpanel.scrollLeft; + var prevScrollTop = scrollpanel.scrollTop; + var panelBounds = scrollpanel.getBoundingClientRect(); + + localStorage.setItem('scale', scale); + zoomout.disabled = (scale == 1); + zoomin.disabled = (scale >= maxScale); + if (srcImage) + repaint(); + + console.log('prev: ' + prevScrollLeft + ', ' + prevScrollTop); + console.log('factor: ' + (scale / prevScale)); + var newLeft = prevScrollLeft * (scale / prevScale); + var newTop = prevScrollTop * (scale / prevScale); + console.log('newLeft: ' + newLeft); + console.log('newTop: ' + newTop); + if (scale > prevScale) { + newLeft += panelBounds.width / 2; + newTop += panelBounds.height / 2; + console.log('c newLeft: ' + newLeft); + console.log('c newTop: ' + newTop); + } else if (scale < prevScale) { + newLeft -= panelBounds.width / 4; + newTop -= panelBounds.height / 4; + console.log('c newLeft: ' + newLeft); + console.log('c newTop: ' + newTop); + } + scrollpanel.scrollLeft = newLeft; + scrollpanel.scrollTop = newTop; + prevScale = scale; +} + +var scalevalue = localStorage.getItem('scale'); +scale = parseInt(scalevalue, 10); +if (!scale || scale < 1 || scale > maxScale) + scale = 4; +prevScale = scale; + +updateZoom(); + +function onZoomIn() { + if (scale < maxScale) + scale *= 2; + updateZoom(); +} + +function onZoomOut() { + if (scale > 1) + scale /= 2; + updateZoom(); +} + +zoomin.addEventListener('click', () => { + onZoomIn(); +}); + +zoomout.addEventListener('click', () => { + onZoomOut(); +}); + +document.addEventListener('keydown', function(e) { + if (e.key == '+' || e.key == '=') { + onZoomIn(); + } + if (e.key == '-') { + onZoomOut(); + } + + console.log(e.key); +}); diff --git a/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_as.xtb b/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_as.xtb index 5285dcc23f5..75516aee16e 100644 --- a/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_as.xtb +++ b/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_as.xtb @@ -19,7 +19,7 @@ <translation id="2648340354586434750">শব্দ অনুসৰি আঁতৰাবলৈ <span class=’key’>Option</span> ধৰি থাকক।</translation> <translation id="2795227192542594043">এই এক্সটেনশ্বনটোৱে আপোনাক ৱেব পৃষ্ঠাত এটা স্থানান্তৰযোগ্য কাৰ্ছৰ প্ৰদান কৰে যি আপোনাক কীব’ৰ্ডৰ দ্বাৰা পাঠ বাছনি কৰাৰ সুবিধা প্ৰদান কৰে।</translation> <translation id="2808027189040546825">১ম প্ৰদক্ষেপ: আটাইতকৈ বেছি ধূসৰ তৰাযুক্ত শাৰীটো বাছনি কৰক:</translation> -<translation id="2965611304828530558"><p>আপুনি যেতিয়া কোনো লিংক বা নিয়ন্ত্ৰণ গৈ পায় তেতিয়া তাত স্বয়ংক্ৰিয়ভাৱে ফ’কাছ কৰা হয়। কোনো লিংক বা বুটামত ক্লিক কৰিবলৈ <span class=’key’>Enter</span> টিপক। </p> <p> যেতিয়া কোনো ফ’কাছ কৰি ৰখা নিয়ন্ত্ৰণে (যেনে পাঠ বাকচ বা সূচীৰ বাকচ) কাঁড় কী কেপচাৰ কৰি থাকে তেতিয়া , কাৰেট ব্ৰাউজিং অব্যাহত ৰাখিবলৈ বাওঁ বা সোঁ কাঁড়ৰ পিছত <span class=’key’>Esc</span>টিপক। </p> <p> ইয়াৰ পৰিৱৰ্তে পৰৱৰ্তী ফ’কাছ কৰিবপৰা নিয়ন্ত্ৰণলৈ যাবলৈ <span class=’key’>Tab</span> টিপক। </p></translation> +<translation id="2965611304828530558"><p>আপুনি যেতিয়া কোনো লিংক বা নিয়ন্ত্ৰণ গৈ পায় তেতিয়া তাত স্বয়ংক্ৰিয়ভাৱে ফ’কাছ কৰা হয়। কোনো লিংক বা বুটামত ক্লিক কৰিবলৈ <span class=’key’>Enter</span> টিপক। </p> <p> যেতিয়া কোনো ফ’কাছ কৰি ৰখা নিয়ন্ত্ৰণে (যেনে পাঠ বাকচ বা সূচীৰ বাকচ) কাঁড় কী কেপচাৰ কৰি থাকে তেতিয়া , কাৰেট ব্ৰাউজিং অব্যাহত ৰাখিবলৈ বাওঁ বা সোঁ কাঁড়ৰ পাছত <span class=’key’>Esc</span>টিপক। </p> <p> ইয়াৰ পৰিৱৰ্তে পৰৱৰ্তী ফ’কাছ কৰিবপৰা নিয়ন্ত্ৰণলৈ যাবলৈ <span class=’key’>Tab</span> টিপক। </p></translation> <translation id="3252573918265662711">ছেট আপ</translation> <translation id="3410969471888629217">ছাইট কাষ্টমাইজেশ্বনৰ সুবিধাটো পাহৰি যাওক</translation> <translation id="3435896845095436175">সক্ষম কৰক</translation> diff --git a/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_te.xtb b/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_te.xtb index 63539df4ebb..e603638b369 100644 --- a/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_te.xtb +++ b/chromium/ui/accessibility/extensions/strings/accessibility_extensions_strings_te.xtb @@ -13,7 +13,7 @@ <translation id="1996252509865389616">ప్రారంభించాలా?</translation> <translation id="2079545284768500474">చర్య రద్దు</translation> <translation id="2179565792157161713">సుదీర్ఘ వివరణను కొత్త ట్యాబ్లో తెరువు</translation> -<translation id="2223143012868735942">రంగు గ్రాహ్యతను మెరుగుపరచడానికి వెబ్పేజీలకు వర్తింపజేసే అనుకూలీకరించగల రంగు ఫిల్టర్.</translation> +<translation id="2223143012868735942">రంగు గ్రాహ్యతను మెరుగుపరచడానికి వెబ్పేజీలకు వర్తింపజేసే అనుకూలంగా మార్చగల రంగు ఫిల్టర్.</translation> <translation id="2394933097471027016">ఇప్పుడే దీన్ని ప్రయత్నించండి - కేరెట్ బ్రౌజింగ్ ఈ పేజీలో ఎల్లప్పుడూ ప్రారంభించబడి ఉంటుంది!</translation> <translation id="2471847333270902538"><ph name="SITE" /> కోసం రంగు స్కీమ్:</translation> <translation id="2648340354586434750">పదాల వారీగా తరలించడానికి <span class='key'>Option</span>ని నొక్కి పట్టుకోండి.</translation> diff --git a/chromium/ui/accessibility/mojom/BUILD.gn b/chromium/ui/accessibility/mojom/BUILD.gn index 3302ef749a9..af01cc903fb 100644 --- a/chromium/ui/accessibility/mojom/BUILD.gn +++ b/chromium/ui/accessibility/mojom/BUILD.gn @@ -5,6 +5,7 @@ import("//mojo/public/tools/bindings/mojom.gni") mojom("mojom") { + generate_java = true sources = [ "ax_action_data.mojom", "ax_event.mojom", @@ -26,7 +27,27 @@ mojom("mojom") { "//url/mojom:url_mojom_gurl", ] - cpp_typemaps = [ + common_typemaps = [ + { + types = [ + { + mojom = "ax.mojom.AXRelativeBounds" + cpp = "::ui::AXRelativeBounds" + }, + ] + traits_headers = [ "ax_relative_bounds_mojom_traits.h" ] + traits_public_deps = [ + ":mojom_traits", + "//ui/gfx", + "//ui/gfx/geometry/mojom", + "//ui/gfx/geometry/mojom:mojom_traits", + "//ui/gfx/mojom", + ] + }, + ] + + cpp_typemaps = common_typemaps + cpp_typemaps += [ { types = [ { @@ -78,12 +99,10 @@ mojom("mojom") { cpp = "::ui::AXRelativeBounds" }, ] - traits_sources = [ "ax_relative_bounds_mojom_traits.cc" ] traits_headers = [ "ax_relative_bounds_mojom_traits.h" ] traits_public_deps = [ - "//ui/gfx", + "//ui/accessibility:ax_base", "//ui/gfx/geometry/mojom", - "//ui/gfx/geometry/mojom:mojom_traits", "//ui/gfx/mojom", ] }, @@ -121,7 +140,10 @@ mojom("mojom") { traits_public_deps = [ "//ui/accessibility:ax_base" ] }, ] + + blink_cpp_typemaps = common_typemaps } + mojom("ax_assistant_mojom") { sources = [ "ax_assistant_structure.mojom" ] @@ -160,3 +182,17 @@ mojom("ax_assistant_mojom") { "//url/mojom:url_mojom_gurl", ] } + +source_set("mojom_traits") { + sources = [ + "ax_relative_bounds_mojom_traits.cc", + "ax_relative_bounds_mojom_traits.h", + ] + public_deps = [ + "//ui/accessibility:ax_base", + "//ui/accessibility/mojom:mojom_shared_cpp_sources", + "//ui/gfx", + "//ui/gfx/geometry/mojom:mojom_traits", + "//ui/gfx/mojom:mojom", + ] +} diff --git a/chromium/ui/accessibility/platform/BUILD.gn b/chromium/ui/accessibility/platform/BUILD.gn index 052fb4b18db..f602ee43c03 100644 --- a/chromium/ui/accessibility/platform/BUILD.gn +++ b/chromium/ui/accessibility/platform/BUILD.gn @@ -87,6 +87,7 @@ source_set("platform") { public_deps = [ "//ui/accessibility:ax_base", + "//ui/base:buildflags", "//ui/display", ] @@ -180,6 +181,8 @@ source_set("platform") { "ax_utils_mac.mm", "inspect/ax_call_statement_invoker_mac.h", "inspect/ax_call_statement_invoker_mac.mm", + "inspect/ax_element_wrapper_mac.h", + "inspect/ax_element_wrapper_mac.mm", "inspect/ax_event_recorder_mac.h", "inspect/ax_event_recorder_mac.mm", "inspect/ax_inspect_utils_mac.h", diff --git a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc index 7cae2b153b7..34f8ee47116 100644 --- a/chromium/ui/accessibility/platform/ax_fragment_root_win.cc +++ b/chromium/ui/accessibility/platform/ax_fragment_root_win.cc @@ -4,8 +4,7 @@ #include "ui/accessibility/platform/ax_fragment_root_win.h" -#include <unordered_map> - +#include "base/containers/flat_map.h" #include "base/no_destructor.h" #include "base/strings/string_number_conversions.h" #include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" @@ -252,7 +251,7 @@ class AXFragmentRootMapWin { } private: - std::unordered_map<gfx::AcceleratedWidget, AXFragmentRootWin*> map_; + base::flat_map<gfx::AcceleratedWidget, AXFragmentRootWin*> map_; }; AXFragmentRootWin::AXFragmentRootWin(gfx::AcceleratedWidget widget, diff --git a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc index 889f7496405..1635f46a8b9 100644 --- a/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc +++ b/chromium/ui/accessibility/platform/ax_platform_atk_hyperlink.cc @@ -7,6 +7,7 @@ #include <string> #include <utility> +#include "ui/accessibility/ax_enum_localization_util.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/platform/ax_platform_node_auralinux.h" #include "ui/accessibility/platform/ax_platform_node_delegate.h" @@ -115,120 +116,6 @@ static void AXPlatformAtkHyperlinkClassInit(AtkHyperlinkClass* klass) { klass->get_end_index = AXPlatformAtkHyperlinkGetEndIndex; } -// -// AtkAction interface. -// - -static AXPlatformNodeAuraLinux* ToAXPlatformNodeAuraLinuxFromHyperlinkAction( - AtkAction* atk_action) { - if (!IS_AX_PLATFORM_ATK_HYPERLINK(atk_action)) - return nullptr; - - return ToAXPlatformNodeAuraLinux(AX_PLATFORM_ATK_HYPERLINK(atk_action)); -} - -static gboolean ax_platform_atk_hyperlink_do_action(AtkAction* action, - gint index) { - g_return_val_if_fail(ATK_IS_ACTION(action), FALSE); - g_return_val_if_fail(!index, FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(action); - if (!obj) - return FALSE; - - obj->DoDefaultAction(); - - return TRUE; -} - -static gint ax_platform_atk_hyperlink_get_n_actions(AtkAction* action) { - g_return_val_if_fail(ATK_IS_ACTION(action), FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(action); - if (!obj) - return 0; - - return 1; -} - -static const gchar* ax_platform_atk_hyperlink_get_description(AtkAction* action, - gint index) { - g_return_val_if_fail(ATK_IS_ACTION(action), FALSE); - g_return_val_if_fail(!index, FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(action); - if (!obj) - return nullptr; - - // Not implemented - return nullptr; -} - -static const gchar* ax_platform_atk_hyperlink_get_keybinding(AtkAction* action, - gint index) { - g_return_val_if_fail(ATK_IS_ACTION(action), FALSE); - g_return_val_if_fail(!index, FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(action); - if (!obj) - return nullptr; - - return obj->GetStringAttribute(ax::mojom::StringAttribute::kAccessKey) - .c_str(); -} - -static const gchar* ax_platform_atk_hyperlink_get_name(AtkAction* atk_action, - gint index) { - g_return_val_if_fail(ATK_IS_ACTION(atk_action), FALSE); - g_return_val_if_fail(!index, FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(atk_action); - if (!obj) - return nullptr; - - int action; - if (!obj->GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb, - &action)) - return nullptr; - std::string action_verb = - ui::ToString(static_cast<ax::mojom::DefaultActionVerb>(action)); - ATK_AURALINUX_RETURN_STRING(action_verb); -} - -static const gchar* ax_platform_atk_hyperlink_get_localized_name( - AtkAction* atk_action, - gint index) { - g_return_val_if_fail(ATK_IS_ACTION(atk_action), FALSE); - g_return_val_if_fail(!index, FALSE); - - AXPlatformNodeAuraLinux* obj = - ToAXPlatformNodeAuraLinuxFromHyperlinkAction(atk_action); - if (!obj) - return nullptr; - - int action; - if (!obj->GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb, - &action)) - return nullptr; - std::string action_verb = - ui::ToLocalizedString(static_cast<ax::mojom::DefaultActionVerb>(action)); - ATK_AURALINUX_RETURN_STRING(action_verb); -} - -static void atk_action_interface_init(AtkActionIface* iface) { - iface->do_action = ax_platform_atk_hyperlink_do_action; - iface->get_n_actions = ax_platform_atk_hyperlink_get_n_actions; - iface->get_description = ax_platform_atk_hyperlink_get_description; - iface->get_keybinding = ax_platform_atk_hyperlink_get_keybinding; - iface->get_name = ax_platform_atk_hyperlink_get_name; - iface->get_localized_name = ax_platform_atk_hyperlink_get_localized_name; -} - void ax_platform_atk_hyperlink_set_object( AXPlatformAtkHyperlink* atk_hyperlink, AXPlatformNodeAuraLinux* platform_node) { @@ -263,13 +150,8 @@ GType ax_platform_atk_hyperlink_get_type() { nullptr /* value table */ }; - static const GInterfaceInfo actionInfo = { - (GInterfaceInitFunc)(GInterfaceInitFunc)atk_action_interface_init, - (GInterfaceFinalizeFunc)0, 0}; - GType type = g_type_register_static( ATK_TYPE_HYPERLINK, "AXPlatformAtkHyperlink", &tinfo, GTypeFlags(0)); - g_type_add_interface_static(type, ATK_TYPE_ACTION, &actionInfo); g_once_init_leave(&type_volatile, type); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node.cc b/chromium/ui/accessibility/platform/ax_platform_node.cc index 1c72c838ae2..a0c15c29d8b 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node.cc @@ -94,7 +94,6 @@ void AXPlatformNode::RemoveAXModeObserver(AXModeObserver* observer) { // static void AXPlatformNode::NotifyAddAXModeFlags(AXMode mode_flags) { - // Note: this is only called on Windows, and in tests. AXMode new_ax_mode(ax_mode_); new_ax_mode |= mode_flags; @@ -108,7 +107,6 @@ void AXPlatformNode::NotifyAddAXModeFlags(AXMode mode_flags) { // static void AXPlatformNode::SetAXMode(AXMode new_mode) { - // Note: this is only called on Windows. ax_mode_ = new_mode; } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc index 5254242e25e..9650287fb80 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.cc @@ -3,6 +3,7 @@ // found in the LICENSE file. #include "ui/accessibility/platform/ax_platform_node_auralinux.h" +#include "base/memory/raw_ptr.h" #include <dlfcn.h> #include <stdint.h> @@ -125,7 +126,7 @@ typedef struct _AXPlatformNodeAuraLinuxClass AXPlatformNodeAuraLinuxClass; struct _AXPlatformNodeAuraLinuxObject { AtkObject parent; - AXPlatformNodeAuraLinux* m_object; + raw_ptr<AXPlatformNodeAuraLinux> m_object; }; struct _AXPlatformNodeAuraLinuxClass { @@ -330,8 +331,10 @@ const char* BuildDescriptionFromHeaders(AXPlatformNodeDelegate* delegate, std::vector<std::string> names; for (const auto& node_id : ids) { if (AXPlatformNode* header = delegate->GetFromNodeID(node_id)) { - if (AtkObject* atk_header = header->GetNativeViewAccessible()) - names.push_back(atk_object_get_name(atk_header)); + if (AtkObject* atk_header = header->GetNativeViewAccessible()) { + if (const gchar* name = atk_object_get_name(atk_header)) + names.push_back(name); + } } } @@ -606,13 +609,10 @@ gboolean DoAction(AtkAction* atk_action, gint index) { if (!obj) return FALSE; - const std::vector<ax::mojom::Action> actions = obj->GetSupportedActions(); + const std::vector<ax::mojom::Action> actions = + obj->GetDelegate()->GetSupportedActions(); g_return_val_if_fail(index < static_cast<gint>(actions.size()), FALSE); - if (index == 0 && obj->HasDefaultActionVerb()) { - // If there is a default action, it will always be at index 0. - return obj->DoDefaultAction(); - } AXActionData data; data.action = actions[index]; return obj->GetDelegate()->AccessibilityPerformAction(data); @@ -627,7 +627,7 @@ gint GetNActions(AtkAction* atk_action) { if (!obj) return 0; - return static_cast<gint>(obj->GetSupportedActions().size()); + return static_cast<gint>(obj->GetDelegate()->GetSupportedActions().size()); } const gchar* GetDescription(AtkAction*, gint) { @@ -645,10 +645,11 @@ const gchar* GetName(AtkAction* atk_action, gint index) { if (!obj) return nullptr; - const std::vector<ax::mojom::Action> actions = obj->GetSupportedActions(); + const std::vector<ax::mojom::Action> actions = + obj->GetDelegate()->GetSupportedActions(); g_return_val_if_fail(index < static_cast<gint>(actions.size()), nullptr); - if (index == 0 && obj->HasDefaultActionVerb()) { + if (index == 0 && obj->GetDelegate()->HasDefaultActionVerb()) { // If there is a default action, it will always be at index 0. return obj->GetDefaultActionName(); } @@ -664,10 +665,11 @@ const gchar* GetKeybinding(AtkAction* atk_action, gint index) { if (!obj) return nullptr; - const std::vector<ax::mojom::Action> actions = obj->GetSupportedActions(); + const std::vector<ax::mojom::Action> actions = + obj->GetDelegate()->GetSupportedActions(); g_return_val_if_fail(index < static_cast<gint>(actions.size()), nullptr); - if (index == 0 && obj->HasDefaultActionVerb()) { + if (index == 0 && obj->GetDelegate()->HasDefaultActionVerb()) { // If there is a default action, it will always be at index 0. Only the // default action has a key binding. return obj->GetStringAttribute(ax::mojom::StringAttribute::kAccessKey) @@ -1013,10 +1015,11 @@ gunichar GetCharacterAtOffset(AtkText* atk_text, int offset) { return 0; std::u16string text = obj->GetHypertext(); - int32_t text_length = text.length(); + size_t text_length = text.length(); offset = obj->UnicodeToUTF16OffsetInText(offset); - int32_t limited_offset = base::clamp(offset, 0, text_length); + offset = std::max(offset, 0); + size_t limited_offset = std::min(static_cast<size_t>(offset), text_length); base_icu::UChar32 code_point; base::ReadUnicodeCharacter(text.c_str(), text_length + 1, &limited_offset, @@ -3892,14 +3895,7 @@ void AXPlatformNodeAuraLinux::EmitCaretChangedSignal() { return; } -#if DCHECK_IS_ON() - AXTree::Selection unignored_selection = - GetDelegate()->GetUnignoredSelection(); - DCHECK(HasCaret(&unignored_selection)); -#endif - std::pair<int, int> selection = GetSelectionOffsetsForAtk(); - AtkObject* atk_object = GetOrCreateAtkObject(); if (!atk_object) return; @@ -4343,8 +4339,8 @@ AXPlatformNodeAuraLinux::GetHypertextAdjustments() { text_unicode_adjustments_.emplace(); std::u16string text = GetHypertext(); - int32_t text_length = text.size(); - for (int32_t i = 0; i < text_length; i++) { + size_t text_length = text.size(); + for (size_t i = 0; i < text_length; i++) { base_icu::UChar32 code_point; size_t original_i = i; base::ReadUnicodeCharacter(text.c_str(), text_length + 1, &i, &code_point); @@ -4618,12 +4614,6 @@ bool AXPlatformNodeAuraLinux:: return false; } -bool AXPlatformNodeAuraLinux::DoDefaultAction() { - AXActionData action_data; - action_data.action = ax::mojom::Action::kDoDefault; - return delegate_->AccessibilityPerformAction(action_data); -} - const gchar* AXPlatformNodeAuraLinux::GetDefaultActionName() { int action; if (!GetIntAttribute(ax::mojom::IntAttribute::kDefaultActionVerb, &action)) @@ -4732,9 +4722,7 @@ bool AXPlatformNodeAuraLinux::IsNameExposed() { } int AXPlatformNodeAuraLinux::GetCaretOffset() { - AXTree::Selection unignored_selection = - GetDelegate()->GetUnignoredSelection(); - if (!HasCaret(&unignored_selection)) { + if (!HasVisibleCaretOrSelection()) { absl::optional<FindInPageResultInfo> result = GetSelectionOffsetsFromFindInPage(); AtkObject* atk_object = GetOrCreateAtkObject(); @@ -5221,28 +5209,4 @@ std::pair<int, int> AXPlatformNodeAuraLinux::GetSelectionOffsetsForAtk() { return selection; } -bool AXPlatformNodeAuraLinux::HasDefaultActionVerb() const { - return GetData().GetDefaultActionVerb() != - ax::mojom::DefaultActionVerb::kNone; -} - -std::vector<ax::mojom::Action> AXPlatformNodeAuraLinux::GetSupportedActions() - const { - static const base::NoDestructor<std::vector<ax::mojom::Action>> - kActionsThatCanBeExposedViaAtkAction{ - {ax::mojom::Action::kDecrement, ax::mojom::Action::kIncrement}}; - std::vector<ax::mojom::Action> supported_actions; - - // The default action, if it exists, must be listed at index 0. - if (HasDefaultActionVerb()) - supported_actions.push_back(ax::mojom::Action::kDoDefault); - - for (const auto& item : *kActionsThatCanBeExposedViaAtkAction) { - if (HasAction(item)) - supported_actions.push_back(item); - } - - return supported_actions; -} - } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h index c71f1359aea..1635ac83ae5 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux.h @@ -154,7 +154,6 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { bool GrabFocusOrSetSequentialFocusNavigationStartingPointAtOffset(int offset); bool GrabFocusOrSetSequentialFocusNavigationStartingPoint(); bool SetSequentialFocusNavigationStartingPoint(); - bool DoDefaultAction(); const gchar* GetDefaultActionName(); AtkAttributeSet* GetAtkAttributes(); @@ -293,9 +292,6 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase { // nullopt. absl::optional<std::pair<int, int>> GetEmbeddedObjectIndices(); - std::vector<ax::mojom::Action> GetSupportedActions() const; - bool HasDefaultActionVerb() const; - std::string accessible_name_; protected: diff --git a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc index e2c5a8e317e..bd381d55d09 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_auralinux_unittest.cc @@ -7,6 +7,7 @@ #include <utility> #include <vector> +#include "base/memory/raw_ptr.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/ax_enums.mojom.h" #include "ui/accessibility/platform/atk_util_auralinux.h" @@ -1000,6 +1001,7 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetNActions) { root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick); root.AddAction(ax::mojom::Action::kDecrement); root.AddAction(ax::mojom::Action::kIncrement); + // Additionally, any object will have a context menu action, that makes it 4 Init(root); AtkObject* root_obj(GetRootAtkObject()); @@ -1009,7 +1011,7 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetNActions) { gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj)); - EXPECT_EQ(3, number_of_actions); + EXPECT_EQ(4, number_of_actions); g_object_unref(root_obj); } @@ -1027,7 +1029,9 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetNActionsNoActions) { gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj)); - EXPECT_EQ(0, number_of_actions); + // In absence of any other actions, we would expose the default and the + // context menu actions. + EXPECT_EQ(2, number_of_actions); g_object_unref(root_obj); } @@ -1047,14 +1051,16 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionGetName) { g_object_ref(root_obj); const gchar* action_name = atk_action_get_name(ATK_ACTION(root_obj), 0); - // The index 0 is reserved for the default action. The rest of actions are - // presented in the order they were added. + // The index 0 is reserved for the default action, and the index 1 to the + // context menu action. The rest of actions are presented in the order they + // were added. EXPECT_STREQ("click", action_name); action_name = atk_action_get_name(ATK_ACTION(root_obj), 1); - EXPECT_STREQ("decrement", action_name); + EXPECT_STREQ("showContextMenu", action_name); action_name = atk_action_get_name(ATK_ACTION(root_obj), 2); + EXPECT_STREQ("decrement", action_name); + action_name = atk_action_get_name(ATK_ACTION(root_obj), 3); EXPECT_STREQ("increment", action_name); - atk_action_do_action(ATK_ACTION(root_obj), 2); g_object_unref(root_obj); } @@ -1078,10 +1084,11 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkActionDoAction) { EXPECT_EQ(root_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 1)); EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 2)); + EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 3)); // Test that querying actions out of bounds doesn't crash EXPECT_FALSE(atk_action_do_action(ATK_ACTION(root_obj), -1)); - EXPECT_FALSE(atk_action_do_action(ATK_ACTION(root_obj), 3)); + EXPECT_FALSE(atk_action_do_action(ATK_ACTION(root_obj), 4)); g_object_unref(root_obj); } @@ -1258,6 +1265,38 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkHyperlink) { g_object_unref(root_obj); } +TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkHyperlinkActions) { + AXNodeData root; + root.id = 1; + root.role = ax::mojom::Role::kLink; + root.AddStringAttribute(ax::mojom::StringAttribute::kUrl, "http://foo.com"); + root.SetDefaultActionVerb(ax::mojom::DefaultActionVerb::kClick); + Init(root); + + AtkObject* root_obj(GetRootAtkObject()); + ASSERT_TRUE(ATK_IS_OBJECT(root_obj)); + ASSERT_TRUE(ATK_IS_HYPERLINK_IMPL(root_obj)); + ASSERT_TRUE(ATK_IS_ACTION(root_obj)); + g_object_ref(root_obj); + auto* root_node = GetRootAsAXNode(); + + gint number_of_actions = atk_action_get_n_actions(ATK_ACTION(root_obj)); + EXPECT_EQ(2, number_of_actions); + + // The index 0 is reserved for the default action, and the index 1 to the + // context menu action. The rest of actions are presented in the order they + // were added. + const gchar* action_name = atk_action_get_name(ATK_ACTION(root_obj), 0); + EXPECT_STREQ("click", action_name); + action_name = atk_action_get_name(ATK_ACTION(root_obj), 1); + EXPECT_STREQ("showContextMenu", action_name); + + EXPECT_TRUE(atk_action_do_action(ATK_ACTION(root_obj), 0)); + EXPECT_EQ(root_node, TestAXNodeWrapper::GetNodeFromLastDefaultAction()); + + g_object_unref(root_obj); +} + // // AtkText interface // @@ -1738,7 +1777,7 @@ class ActivationTester { g_signal_handler_disconnect(target_, deactivate_id_); } - AtkObject* target_; + raw_ptr<AtkObject> target_; bool saw_activate_ = false; bool saw_deactivate_ = false; gulong activate_id_ = 0; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_base.cc index 7ec9cd89979..0bf97a38d7c 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.cc @@ -10,9 +10,10 @@ #include <set> #include <sstream> #include <string> -#include <unordered_map> +#include "base/containers/flat_map.h" #include "base/no_destructor.h" +#include "base/numerics/checked_math.h" #include "base/strings/string_number_conversions.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" @@ -65,7 +66,7 @@ bool FindDescendantRoleWithMaxDepth(const AXPlatformNodeBase* node, const char16_t AXPlatformNodeBase::kEmbeddedCharacter = u'\xfffc'; // Map from each AXPlatformNode's unique id to its instance. -using UniqueIdMap = std::unordered_map<int32_t, AXPlatformNode*>; +using UniqueIdMap = base::flat_map<int32_t, AXPlatformNode*>; base::LazyInstance<UniqueIdMap>::Leaky g_unique_id_map = LAZY_INSTANCE_INITIALIZER; @@ -193,6 +194,7 @@ std::string AXPlatformNodeBase::GetName() const { name += extra_text; } + DCHECK(base::IsStringUTF8AllowingNoncharacters(name)) << "Invalid UTF8"; return name; } return std::string(); @@ -824,10 +826,6 @@ bool AXPlatformNodeBase::IsPlatformDocument() const { return delegate_ && delegate_->IsPlatformDocument(); } -bool AXPlatformNodeBase::IsPlatformDocumentWithContent() const { - return delegate_ && delegate_->IsPlatformDocumentWithContent(); -} - bool AXPlatformNodeBase::IsStructuredAnnotation() const { // The node represents a structured annotation if it can trace back to a // target node that is being annotated. @@ -1115,27 +1113,8 @@ absl::optional<float> AXPlatformNodeBase::GetFontSizeInPoints() const { return absl::nullopt; } -bool AXPlatformNodeBase::HasCaret(const AXTree::Selection* selection) { - if (IsAtomicTextField() && - HasIntAttribute(ax::mojom::IntAttribute::kTextSelStart) && - HasIntAttribute(ax::mojom::IntAttribute::kTextSelEnd)) { - return true; - } - - // The caret is always at the focus of the selection. - int32_t focus_id; - if (selection) - focus_id = selection->focus_object_id; - else - focus_id = delegate_->GetTreeData().sel_focus_object_id; - - AXPlatformNodeBase* focus_object = - static_cast<AXPlatformNodeBase*>(delegate_->GetFromNodeID(focus_id)); - - if (!focus_object) - return false; - - return focus_object->IsDescendantOf(this); +bool AXPlatformNodeBase::HasVisibleCaretOrSelection() const { + return delegate_ && delegate_->HasVisibleCaretOrSelection(); } bool AXPlatformNodeBase::IsLeaf() const { @@ -1153,7 +1132,7 @@ bool AXPlatformNodeBase::IsInvisibleOrIgnored() const { if (HasState(ax::mojom::State::kFocusable)) return !IsFocused(); - return !const_cast<AXPlatformNodeBase*>(this)->HasCaret(); + return !HasVisibleCaretOrSelection(); } bool AXPlatformNodeBase::IsFocused() const { @@ -1262,11 +1241,13 @@ void AXPlatformNodeBase::ComputeAttributes(PlatformAttributeList* attributes) { case ax::mojom::DescriptionFrom::kPopupElement: // The following types of markup are mapped to "tooltip": // * The title attribute. - // * A related popup=hint related via togglepopup/showpopup/hidepopup. + // * A related popup=hint related via popuptoggletarget / + // popupshowtarget / popuphidetarget. // * A tooltip related via aria-describedby (see kRelatedElement above). from = "tooltip"; break; case ax::mojom::DescriptionFrom::kNone: + case ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty: NOTREACHED(); } DCHECK(!from.empty()); @@ -1777,6 +1758,31 @@ int32_t AXPlatformNodeBase::GetHypertextOffsetFromChild( return GetHypertextOffsetFromHyperlinkIndex(hyperlink_index); } +int AXPlatformNodeBase::HypertextOffsetFromChildIndex(int child_index) const { + DCHECK_GE(child_index, 0); + DCHECK_LE(child_index, static_cast<int>(GetChildCount())); + + // Use both a child index and an iterator to avoid an O(n^2) complexity which + // would be the case if we were to call GetChildAtIndex on each child. + int hypertext_offset = 0; + int endpoint_child_index = 0; + for (AXPlatformNodeChildIterator child_iter = AXPlatformNodeChildrenBegin(); + child_iter != AXPlatformNodeChildrenEnd(); ++child_iter) { + if (endpoint_child_index >= child_index) { + break; + } + + int child_text_len = 1; + if (child_iter->IsText()) + child_text_len = + base::checked_cast<int>(child_iter->GetHypertext().size()); + + endpoint_child_index++; + hypertext_offset += child_text_len; + } + return hypertext_offset; +} + int32_t AXPlatformNodeBase::GetHypertextOffsetFromDescendant( AXPlatformNodeBase* descendant) { auto* parent_object = static_cast<AXPlatformNodeBase*>( @@ -1795,44 +1801,50 @@ int32_t AXPlatformNodeBase::GetHypertextOffsetFromDescendant( int AXPlatformNodeBase::GetHypertextOffsetFromEndpoint( AXPlatformNodeBase* endpoint_object, int endpoint_offset) { + DCHECK_GE(endpoint_offset, 0); + // There are three cases: - // 1. The selection endpoint is inside this object but not one of its - // descendants, or is in an ancestor of this object. endpoint_offset should be + // 1. The selection endpoint is this object itself: endpoint_offset should be // returned, possibly adjusted from a child offset to a hypertext offset. - // 2. The selection endpoint is a descendant of this object. The offset of the + // 2. The selection endpoint is an ancestor of this object. If endpoint_offset + // points out after this object, then this object text length is returned, + // otherwise 0. + // 3. The selection endpoint is a descendant of this object. The offset of the // character in this object's hypertext corresponding to the subtree in which // the endpoint is located should be returned. - // 3. The selection endpoint is in a completely different part of the tree. + // 4. The selection endpoint is in a completely different part of the tree. // Either 0 or hypertext length should be returned depending on the direction // that one needs to travel to find the endpoint. // // TODO(nektar): Replace all this logic with the use of AXNodePosition. - // Case 1. Is the endpoint object equal to this object or an ancestor of this - // object? - // - // IsDescendantOf includes the case when endpoint_object == this. - if (IsDescendantOf(endpoint_object)) { - if (endpoint_object->IsLeaf()) { - DCHECK_EQ(endpoint_object, this) << "Text objects cannot have children."; + // Case 1. Is the endpoint object equal to this object + if (endpoint_object == this) { + if (endpoint_object->IsLeaf()) return endpoint_offset; - } else { - DCHECK_GE(endpoint_offset, 0); - DCHECK_LE(static_cast<size_t>(endpoint_offset), - endpoint_object->GetDelegate()->GetChildCount()); - - // Adjust the |endpoint_offset| because the selection endpoint is a tree - // position, i.e. it represents a child index and not a text offset. - if (static_cast<size_t>(endpoint_offset) >= - endpoint_object->GetChildCount()) { - return static_cast<int>(endpoint_object->GetHypertext().size()); - } else { - auto* child = static_cast<AXPlatformNodeBase*>(FromNativeViewAccessible( - endpoint_object->ChildAtIndex(endpoint_offset))); - DCHECK(child); - return endpoint_object->GetHypertextOffsetFromChild(child); - } + return HypertextOffsetFromChildIndex(endpoint_offset); + } + + // Case 2. Is the endpoint an ancestor of this object. + if (IsDescendantOf(endpoint_object)) { + DCHECK_LE(endpoint_offset, + static_cast<int>(endpoint_object->GetChildCount())); + + AXPlatformNodeBase* closest_ancestor = this; + while (closest_ancestor) { + AXPlatformNodeBase* parent = static_cast<AXPlatformNodeBase*>( + FromNativeViewAccessible(closest_ancestor->GetParent())); + if (parent == endpoint_object) + break; + closest_ancestor = parent; } + + // If the endpoint is after this node, then return the node's + // hypertext length, otherwise 0 as the endpoint points before the node. + if (endpoint_offset > + static_cast<int>(*closest_ancestor->GetIndexInParent())) + return static_cast<int>(GetHypertext().size()); + return 0; } AXPlatformNodeBase* common_parent = this; @@ -1895,14 +1907,15 @@ AXPlatformNodeBase::AXPosition AXPlatformNodeBase::HypertextOffsetToEndpoint( DCHECK_GE(hypertext_offset, 0); DCHECK_LT(hypertext_offset, static_cast<int>(GetHypertext().size())); - int32_t current_hypertext_offset = hypertext_offset; + int current_hypertext_offset = hypertext_offset; for (auto child_iter = AXPlatformNodeChildrenBegin(); child_iter != AXPlatformNodeChildrenEnd() && current_hypertext_offset >= 0; ++child_iter) { int child_text_len = 1; if (child_iter->IsText()) - child_text_len = child_iter->GetHypertext().size(); + child_text_len = + base::checked_cast<int>(child_iter->GetHypertext().size()); if (current_hypertext_offset < child_text_len) { int endpoint_offset = child_text_len - current_hypertext_offset; @@ -1989,7 +2002,7 @@ void AXPlatformNodeBase::GetSelectionOffsetsFromTree( // outside this object in their entirety. // Selections that span more than one character are by definition inside // this object, so checking them is not necessary. - if (*selection_start == *selection_end && !HasCaret(selection)) { + if (*selection_start == *selection_end && !HasVisibleCaretOrSelection()) { *selection_start = -1; *selection_end = -1; return; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base.h b/chromium/ui/accessibility/platform/ax_platform_node_base.h index ef0715b28f4..9a78c92ee3c 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_base.h @@ -267,13 +267,8 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // Returns the font size converted to points, if available. absl::optional<float> GetFontSizeInPoints() const; - // Returns true if either a descendant has selection (sel_focus_object_id) or - // if this node is a simple text element and has text selection attributes. - // Optionally accepts a selection, which can be useful if checking the - // unignored selection is required. If not provided, uses the selection from - // the tree data, which is safe and fast but does not take ignored nodes into - // account. - bool HasCaret(const AXTree::Selection* selection = nullptr); + // See `AXNode::HasVisibleCaretOrSelection`. + bool HasVisibleCaretOrSelection() const; // See AXPlatformNodeDelegate::IsChildOfLeaf(). bool IsChildOfLeaf() const; @@ -425,10 +420,6 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { // PDF. bool IsPlatformDocument() const; - // Returns true if this object is a platform document as described above and - // also has at least some content. - bool IsPlatformDocumentWithContent() const; - protected: AXPlatformNodeBase(); @@ -547,6 +538,7 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { int32_t GetHyperlinkIndexFromChild(AXPlatformNodeBase* child); int32_t GetHypertextOffsetFromHyperlinkIndex(int32_t hyperlink_index); int32_t GetHypertextOffsetFromChild(AXPlatformNodeBase* child); + int HypertextOffsetFromChildIndex(int child_index) const; int32_t GetHypertextOffsetFromDescendant(AXPlatformNodeBase* descendant); // If the selection endpoint is either equal to or an ancestor of this object, @@ -592,6 +584,8 @@ class AX_EXPORT AXPlatformNodeBase : public AXPlatformNode { friend AXPlatformNode* AXPlatformNode::Create( AXPlatformNodeDelegate* delegate); + + FRIEND_TEST_ALL_PREFIXES(AXPlatformNodeTest, HypertextOffsetFromEndpoint); }; } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_base_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_base_unittest.cc index 4072a27b660..d19651211ed 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_base_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_base_unittest.cc @@ -7,22 +7,14 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/accessibility/platform/ax_platform_node_unittest.h" #include "ui/accessibility/platform/test_ax_node_wrapper.h" +#include "ui/accessibility/platform/test_ax_tree_update.h" + +using ax::mojom::Role; +using ax::mojom::State; namespace ui { namespace { -void MakeStaticText(AXNodeData* node, int id, const std::string& text) { - node->id = id; - node->role = ax::mojom::Role::kStaticText; - node->SetName(text); -} - -void MakeGroup(AXNodeData* node, int id, std::vector<int> child_ids) { - node->id = id; - node->role = ax::mojom::Role::kGroup; - node->child_ids = child_ids; -} - void SetIsInvisible(AXTree* tree, int id, bool invisible) { AXTreeUpdate update; update.nodes.resize(1); @@ -45,33 +37,18 @@ void SetRole(AXTree* tree, int id, ax::mojom::Role role) { } // namespace TEST_F(AXPlatformNodeTest, GetHypertext) { - AXTreeUpdate update; - // RootWebArea #1 // ++++StaticText "text1" #2 // ++++StaticText "text2" #3 // ++++StaticText "text3" #4 - - update.root_id = 1; - update.nodes.resize(4); - - update.nodes[0].id = 1; - update.nodes[0].role = ax::mojom::Role::kRootWebArea; - update.nodes[0].child_ids = {2, 3, 4}; - - MakeStaticText(&update.nodes[1], 2, "text1"); - MakeStaticText(&update.nodes[2], 3, "text2"); - MakeStaticText(&update.nodes[3], 4, "text3"); - - Init(update); - AXTree& tree = *GetTree(); + AXTree* tree = Init({Role::kRootWebArea, {{"text1"}, {"text2"}, {"text3"}}}); // Set an AXMode on the AXPlatformNode as some platforms (auralinux) use it to // determine if it should enable accessibility. testing::ScopedAxModeSetter ax_mode_setter(kAXModeComplete); AXPlatformNodeBase* root = static_cast<AXPlatformNodeBase*>( - TestAXNodeWrapper::GetOrCreate(&tree, tree.root())->ax_platform_node()); + TestAXNodeWrapper::GetOrCreate(tree, tree->root())->ax_platform_node()); EXPECT_EQ(root->GetHypertext(), u"text1text2text3"); @@ -89,8 +66,6 @@ TEST_F(AXPlatformNodeTest, GetHypertext) { } TEST_F(AXPlatformNodeTest, GetHypertextIgnoredContainerSiblings) { - AXTreeUpdate update; - // RootWebArea #1 // ++genericContainer IGNORED #2 // ++++StaticText "text1" #3 @@ -98,41 +73,18 @@ TEST_F(AXPlatformNodeTest, GetHypertextIgnoredContainerSiblings) { // ++++StaticText "text2" #5 // ++genericContainer IGNORED #6 // ++++StaticText "text3" #7 + AXTree* tree = + Init({Role::kRootWebArea, + {{Role::kGenericContainer, State::kIgnored, {{"text1"}}}, + {Role::kGenericContainer, State::kIgnored, {{"text2"}}}, + {Role::kGenericContainer, State::kIgnored, {{"text3"}}}}}); - update.root_id = 1; - update.nodes.resize(7); - - update.nodes[0].id = 1; - update.nodes[0].role = ax::mojom::Role::kRootWebArea; - update.nodes[0].child_ids = {2, 4, 6}; - - update.nodes[1].id = 2; - update.nodes[1].child_ids = {3}; - update.nodes[1].role = ax::mojom::Role::kGenericContainer; - update.nodes[1].AddState(ax::mojom::State::kIgnored); - MakeStaticText(&update.nodes[2], 3, "text1"); - - update.nodes[3].id = 4; - update.nodes[3].child_ids = {5}; - update.nodes[3].role = ax::mojom::Role::kGenericContainer; - update.nodes[3].AddState(ax::mojom::State::kIgnored); - MakeStaticText(&update.nodes[4], 5, "text2"); - - update.nodes[5].id = 6; - update.nodes[5].child_ids = {7}; - update.nodes[5].role = ax::mojom::Role::kGenericContainer; - update.nodes[5].AddState(ax::mojom::State::kIgnored); - MakeStaticText(&update.nodes[6], 7, "text3"); - - Init(update); - - AXTree& tree = *GetTree(); // Set an AXMode on the AXPlatformNode as some platforms (auralinux) use it to // determine if it should enable accessibility. ui::testing::ScopedAxModeSetter ax_mode_setter(kAXModeComplete); AXPlatformNodeBase* root = static_cast<AXPlatformNodeBase*>( - TestAXNodeWrapper::GetOrCreate(&tree, tree.root())->ax_platform_node()); + TestAXNodeWrapper::GetOrCreate(tree, tree->root())->ax_platform_node()); EXPECT_EQ(root->GetHypertext(), u"text1text2text3"); @@ -153,25 +105,16 @@ TEST_F(AXPlatformNodeTest, GetHypertextIgnoredContainerSiblings) { } TEST_F(AXPlatformNodeTest, GetTextContentIgnoresInvisibleAndIgnored) { - AXTreeUpdate update; - - update.root_id = 1; - update.nodes.resize(6); - - MakeStaticText(&update.nodes[1], 2, "a"); - MakeStaticText(&update.nodes[2], 3, "b"); - - MakeStaticText(&update.nodes[4], 5, "d"); - MakeStaticText(&update.nodes[5], 6, "e"); - - MakeGroup(&update.nodes[3], 4, {5, 6}); - MakeGroup(&update.nodes[0], 1, {2, 3, 4}); - - Init(update); - - AXTree& tree = *GetTree(); + // kGroup + // ++kStaticText "a" + // ++kStaticText "b" + // ++kGroup + // ++++kStaticText "d" + // ++++kStaticText "e" + AXTree* tree = + Init({Role::kGroup, {{"a"}, {"b"}, {Role::kGroup, {{"d"}, {"e"}}}}}); auto* root = static_cast<AXPlatformNodeBase*>( - TestAXNodeWrapper::GetOrCreate(&tree, tree.root())->ax_platform_node()); + TestAXNodeWrapper::GetOrCreate(tree, tree->root())->ax_platform_node()); // Set an AXMode on the AXPlatformNode as some platforms (auralinux) use it to // determine if it should enable accessibility. @@ -182,26 +125,26 @@ TEST_F(AXPlatformNodeTest, GetTextContentIgnoresInvisibleAndIgnored) { // Setting invisible or ignored on a static text node causes it to be included // or excluded from the root node's text content: { - SetIsInvisible(&tree, 2, true); + SetIsInvisible(tree, 2, true); EXPECT_EQ(root->GetTextContentUTF16(), u"bde"); - SetIsInvisible(&tree, 2, false); + SetIsInvisible(tree, 2, false); EXPECT_EQ(root->GetTextContentUTF16(), u"abde"); - SetRole(&tree, 2, ax::mojom::Role::kNone); + SetRole(tree, 2, ax::mojom::Role::kNone); EXPECT_EQ(root->GetTextContentUTF16(), u"bde"); - SetRole(&tree, 2, ax::mojom::Role::kStaticText); + SetRole(tree, 2, ax::mojom::Role::kStaticText); EXPECT_EQ(root->GetTextContentUTF16(), u"abde"); } // Setting invisible or ignored on a group node has no effect on the // text content: { - SetIsInvisible(&tree, 4, true); + SetIsInvisible(tree, 4, true); EXPECT_EQ(root->GetTextContentUTF16(), u"abde"); - SetRole(&tree, 4, ax::mojom::Role::kNone); + SetRole(tree, 4, ax::mojom::Role::kNone); EXPECT_EQ(root->GetTextContentUTF16(), u"abde"); } } @@ -573,4 +516,73 @@ TEST_F(AXPlatformNodeTest, CompareTo) { } } } + +TEST_F(AXPlatformNodeTest, HypertextOffsetFromEndpoint) { + // <p> + // <a href="google.com">link</a> + // </p> + // + // kRootWebArea + // ++kParagraph + // ++++kLink + // ++++++kStaticText "link" + // ++++++kStaticText "link#2" + AXTree* tree = + Init({Role::kRootWebArea, + {{Role::kParagraph, {{Role::kLink, {{"link"}, {"link#2"}}}}}}}); + auto* root = static_cast<AXPlatformNodeBase*>( + TestAXNodeWrapper::GetOrCreate(tree, tree->root())->ax_platform_node()); + + // Set an AXMode on the AXPlatformNode as some platforms (auralinux) use it to + // determine if it should enable accessibility. + ui::testing::ScopedAxModeSetter ax_mode_setter(kAXModeComplete); + + auto* paragraph = static_cast<AXPlatformNodeBase*>( + AXPlatformNode::FromNativeViewAccessible(root->ChildAtIndex(0))); + + auto* link = static_cast<AXPlatformNodeBase*>( + AXPlatformNode::FromNativeViewAccessible(paragraph->ChildAtIndex(0))); + + auto* static_text = static_cast<AXPlatformNodeBase*>( + AXPlatformNode::FromNativeViewAccessible(link->ChildAtIndex(0))); + + auto* static_text2 = static_cast<AXPlatformNodeBase*>( + AXPlatformNode::FromNativeViewAccessible(link->ChildAtIndex(1))); + + // End point is a parent, points before/after the link. + { + EXPECT_EQ(link->GetHypertextOffsetFromEndpoint(paragraph, 0), 0); + EXPECT_EQ(link->GetHypertextOffsetFromEndpoint(paragraph, 1), 10); + } + + // End point is a parent, points before/after the static texts. + { + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(link, 0), 0); + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(link, 1), 4); + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(link, 2), 4); + + EXPECT_EQ(static_text2->GetHypertextOffsetFromEndpoint(link, 0), 0); + EXPECT_EQ(static_text2->GetHypertextOffsetFromEndpoint(link, 1), 0); + EXPECT_EQ(static_text2->GetHypertextOffsetFromEndpoint(link, 2), 6); + } + + // End point is a grand parent, points before/after the static text. + { + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(paragraph, 0), 0); + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(paragraph, 1), 4); + } + + // End point is |this|, points into |this| text leaf object. + { + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(static_text, 0), 0); + EXPECT_EQ(static_text->GetHypertextOffsetFromEndpoint(static_text, 4), 4); + } + + // End point is |this|, points into |this| hypertext object. + { + EXPECT_EQ(link->GetHypertextOffsetFromEndpoint(link, 0), 0); + EXPECT_EQ(link->GetHypertextOffsetFromEndpoint(link, 1), 4); + } +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_cocoa.mm b/chromium/ui/accessibility/platform/ax_platform_node_cocoa.mm index 84fa4180d2f..3abaab364ac 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_cocoa.mm +++ b/chromium/ui/accessibility/platform/ax_platform_node_cocoa.mm @@ -301,6 +301,7 @@ bool IsAXSetter(SEL selector) { case ax::mojom::Role::kGenericContainer: case ax::mojom::Role::kGroup: case ax::mojom::Role::kRadioGroup: + case ax::mojom::Role::kTabPanel: return true; default: break; @@ -684,16 +685,22 @@ bool IsAXSetter(SEL selector) { } - (BOOL)isImage { - bool isImage = + bool has_image_semantics = ui::IsImage(_node->GetRole()) && - !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback); - DCHECK(!([[self accessibilityRole] isEqualToString:NSAccessibilityImageRole] ^ - isImage)) - << "Internal and native roles do not match when determining if this " - "object is an image. " - << "Chrome role: " << ui::ToString(_node->GetRole()) - << ", NSAccessibility role: " << [self accessibilityRole]; - return isImage; + !_node->GetBoolAttribute(ax::mojom::BoolAttribute::kCanvasHasFallback) && + !_node->GetChildCount() && + _node->GetNameFrom() != ax::mojom::NameFrom::kAttributeExplicitlyEmpty; +#if DCHECK_IS_ON() + bool is_native_image = + [[self accessibilityRole] isEqualToString:NSAccessibilityImageRole]; + DCHECK_EQ(is_native_image, has_image_semantics) + << "\nPresence/lack of native image role do not match the expected " + "internal semantics:" + << "\n* Chrome role: " << ui::ToString(_node->GetRole()) + << "\n* NSAccessibility role: " << [self accessibilityRole] + << "\n* AXNode: " << *_node; +#endif + return has_image_semantics; } - (NSString*)getName { diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate.cc b/chromium/ui/accessibility/platform/ax_platform_node_delegate.cc index b1d01d7440e..02b953c3be9 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate.cc @@ -4,6 +4,8 @@ #include "ui/accessibility/platform/ax_platform_node_delegate.h" +#include "base/containers/fixed_flat_set.h" + namespace ui { gfx::Rect AXPlatformNodeDelegate::GetClippedScreenBoundsRect( @@ -42,4 +44,38 @@ gfx::Rect AXPlatformNodeDelegate::GetUnclippedFrameBoundsRect( AXClippingBehavior::kUnclipped, offscreen_result); } +bool AXPlatformNodeDelegate::HasDefaultActionVerb() const { + return GetData().GetDefaultActionVerb() != + ax::mojom::DefaultActionVerb::kNone; +} + +std::vector<ax::mojom::Action> AXPlatformNodeDelegate::GetSupportedActions() + const { + static constexpr auto kActionsThatCanBeExposed = + base::MakeFixedFlatSet<ax::mojom::Action>( + {ax::mojom::Action::kDecrement, ax::mojom::Action::kIncrement, + ax::mojom::Action::kScrollUp, ax::mojom::Action::kScrollDown, + ax::mojom::Action::kScrollLeft, ax::mojom::Action::kScrollRight, + ax::mojom::Action::kScrollForward, + ax::mojom::Action::kScrollBackward}); + std::vector<ax::mojom::Action> supported_actions; + + // The default action must be listed at index 0. + // TODO(crbug.com/1370076): Find out why some nodes do not expose a + // default action (HasDefaultActionVerb() is false). + supported_actions.push_back(ax::mojom::Action::kDoDefault); + + // Users expect to be able to bring a context menu on any object via e.g. + // right click, so we make the context menu action available to any object + // unconditionally. + supported_actions.push_back(ax::mojom::Action::kShowContextMenu); + + for (const auto& item : kActionsThatCanBeExposed) { + if (HasAction(item)) + supported_actions.push_back(item); + } + + return supported_actions; +} + } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h index f4b11c48a1d..4e4b7cba0d8 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate.h @@ -168,8 +168,11 @@ class AX_EXPORT AXPlatformNodeDelegate { virtual bool HasState(ax::mojom::State state) const = 0; virtual ax::mojom::State GetState() const = 0; virtual bool HasAction(ax::mojom::Action action) const = 0; + bool HasDefaultActionVerb() const; + std::vector<ax::mojom::Action> GetSupportedActions() const; virtual bool HasTextStyle(ax::mojom::TextStyle text_style) const = 0; virtual ax::mojom::NameFrom GetNameFrom() const = 0; + virtual ax::mojom::DescriptionFrom GetDescriptionFrom() const = 0; // Returns the text of this node and all descendant nodes; including text // found in embedded objects. @@ -186,9 +189,7 @@ class AX_EXPORT AXPlatformNodeDelegate { // field. virtual std::u16string GetValueForControl() const = 0; - // Get the unignored selection from the tree, meaning the selection whose - // endpoints are on unignored nodes. (An ignored node means that the node - // should not be exposed to platform APIs: See `IsIgnored`.) + // See `AXNode::GetUnignoredSelection`. virtual const AXTree::Selection GetUnignoredSelection() const = 0; // Creates a text position rooted at this object if it's a leaf node, or a @@ -270,10 +271,6 @@ class AX_EXPORT AXPlatformNodeDelegate { // PDF. virtual bool IsPlatformDocument() const = 0; - // Returns true if this object is a platform document as described above and - // also has at least some content. - virtual bool IsPlatformDocumentWithContent() const = 0; - // Returns true if this node is ignored and should be hidden from the // accessibility tree. Methods that are used to navigate the accessibility // tree, such as "ChildAtIndex", "GetParent", and "GetChildCount", among @@ -347,6 +344,11 @@ class AX_EXPORT AXPlatformNodeDelegate { // Returns the accessible name for the node. virtual const std::string& GetName() const = 0; + // Returns the accessible description for the node. + // An accessible description gives more information about the node in + // contrast to the accessible name which is a shorter label for the node. + virtual const std::string& GetDescription() const = 0; + // Returns the text of this node and represent the text of descendant nodes // with a special character in place of every embedded object. This represents // the concept of text in ATK and IA2 APIs. @@ -459,7 +461,7 @@ class AX_EXPORT AXPlatformNodeDelegate { // Get whether this node is marked as read-only or is disabled. virtual bool IsReadOnlyOrDisabled() const = 0; - // Returns true if the caret or selection is visible on this object. + // See `AXNode::HasVisibleCaretOrSelection`. virtual bool HasVisibleCaretOrSelection() const = 0; // Get another node from this same tree. @@ -559,8 +561,8 @@ class AX_EXPORT AXPlatformNodeDelegate { // or treegrid. virtual bool IsCellOrHeaderOfAriaGrid() const = 0; - // True if this is a web area, and its grandparent is a presentational iframe. - virtual bool IsWebAreaForPresentationalIframe() const = 0; + // See `AXNode::IsRootWebAreaForPresentationalIframe()`. + virtual bool IsRootWebAreaForPresentationalIframe() const = 0; // Ordered-set-like and item-like nodes. virtual bool IsOrderedSetItem() const = 0; @@ -618,7 +620,7 @@ class AX_EXPORT AXPlatformNodeDelegate { std::string SubtreeToString() { return SubtreeToStringHelper(0u); } friend std::ostream& operator<<(std::ostream& stream, - AXPlatformNodeDelegate& delegate) { + const AXPlatformNodeDelegate& delegate) { return stream << delegate.ToString(); } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc index 14c9080cf1c..58414cdb95a 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.cc @@ -213,6 +213,11 @@ ax::mojom::NameFrom AXPlatformNodeDelegateBase::GetNameFrom() const { return GetData().GetNameFrom(); } +ax::mojom::DescriptionFrom AXPlatformNodeDelegateBase::GetDescriptionFrom() + const { + return GetData().GetDescriptionFrom(); +} + std::u16string AXPlatformNodeDelegateBase::GetTextContentUTF16() const { // Unlike in web content The "kValue" attribute always takes precedence, // because we assume that users of this base class, such as Views controls, @@ -385,10 +390,6 @@ bool AXPlatformNodeDelegateBase::IsPlatformDocument() const { return ui::IsPlatformDocument(GetRole()); } -bool AXPlatformNodeDelegateBase::IsPlatformDocumentWithContent() const { - return IsPlatformDocument() && GetChildCount(); -} - bool AXPlatformNodeDelegateBase::IsDescendantOfAtomicTextField() const { // TODO(nektar): Add const to all tree traversal methods and remove // const_cast. @@ -562,6 +563,10 @@ const std::string& AXPlatformNodeDelegateBase::GetName() const { return GetStringAttribute(ax::mojom::StringAttribute::kName); } +const std::string& AXPlatformNodeDelegateBase::GetDescription() const { + return GetStringAttribute(ax::mojom::StringAttribute::kDescription); +} + std::u16string AXPlatformNodeDelegateBase::GetHypertext() const { return std::u16string(); } @@ -774,14 +779,12 @@ bool AXPlatformNodeDelegateBase::IsCellOrHeaderOfAriaGrid() const { return false; } -bool AXPlatformNodeDelegateBase::IsWebAreaForPresentationalIframe() const { +bool AXPlatformNodeDelegateBase::IsRootWebAreaForPresentationalIframe() const { if (!ui::IsPlatformDocument(GetRole())) return false; - AXPlatformNodeDelegate* parent = GetParentDelegate(); if (!parent) return false; - return parent->GetRole() == ax::mojom::Role::kIframePresentational; } diff --git a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h index 61a378dd299..3113e7144d9 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_delegate_base.h @@ -96,6 +96,7 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { bool HasAction(ax::mojom::Action action) const override; bool HasTextStyle(ax::mojom::TextStyle text_style) const override; ax::mojom::NameFrom GetNameFrom() const override; + ax::mojom::DescriptionFrom GetDescriptionFrom() const override; std::u16string GetTextContentUTF16() const override; std::u16string GetValueForControl() const override; const AXTree::Selection GetUnignoredSelection() const override; @@ -139,7 +140,6 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { bool IsChildOfLeaf() const override; bool IsDescendantOfAtomicTextField() const override; bool IsPlatformDocument() const override; - bool IsPlatformDocumentWithContent() const override; bool IsLeaf() const override; bool IsFocused() const override; bool IsToplevelBrowserWindow() override; @@ -171,6 +171,7 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { std::unique_ptr<AXPlatformNodeDelegate::ChildIterator> ChildrenEnd() override; const std::string& GetName() const override; + const std::string& GetDescription() const override; std::u16string GetHypertext() const override; const std::map<int, int>& GetHypertextOffsetToHyperlinkChildIndex() const override; @@ -318,7 +319,7 @@ class AX_EXPORT AXPlatformNodeDelegateBase : public AXPlatformNodeDelegate { int col_index) const override; absl::optional<int32_t> CellIndexToId(int cell_index) const override; bool IsCellOrHeaderOfAriaGrid() const override; - bool IsWebAreaForPresentationalIframe() const override; + bool IsRootWebAreaForPresentationalIframe() const override; // Ordered-set-like and item-like nodes. bool IsOrderedSetItem() const override; diff --git a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm index c78802354ff..cae2a44f1ca 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_mac.mm +++ b/chromium/ui/accessibility/platform/ax_platform_node_mac.mm @@ -28,8 +28,12 @@ void PostAnnouncementNotification(NSString* announcement, notification_info); } void NotifyMacEvent(AXPlatformNodeCocoa* target, ax::mojom::Event event_type) { - if (![target AXWindow]) + if (![target AXWindow]) { + // A child tree is not attached to the window. Return early, otherwise + // AppKit will hang trying to reach the root, resulting in a bug where + // VoiceOver keeps repeating "[appname] is not responding". return; + } NSString* notification = [AXPlatformNodeCocoa nativeNotificationFromAXEvent:event_type]; if (notification) diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc index 37a21fafde0..ad2a2d7e953 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win.cc @@ -470,6 +470,28 @@ HRESULT AXPlatformNodeTextRangeProviderWin::FindText( WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT); WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXTRANGE_FINDTEXT); UIA_VALIDATE_TEXTRANGEPROVIDER_CALL_1_IN_1_OUT(string, result); + // On Windows, there's a dichotomy in the definition of a text offset in a + // text position between different APIs: + // - on UIA, a text offset translates to the offset in the text itself + // - on IA2, it translates to the offset in the hypertext + // + // All unignored non-text nodes are represented with an "embedded object + // character" in their parent's text representation on IA2, but aren't on UIA. + // This leads to different expected MaxTextOffset values for a same text + // position. If `string` is found in the text represented by the start/end + // endpoints, we'll create text positions in the least common ancestor, use + // the flat text representation's offsets of found string, then convert the + // positions to leaf. If 'embedded object characters' are considered, instead + // of the flat text representation, this falls apart. + // + // Whether we expose embedded object characters for nodes is managed by the + // |g_ax_embedded_object_behavior| global variable set in ax_node_position.cc. + // When on Windows, this variable is always set to kExposeCharacter... which + // is incorrect if we run UIA-specific code. To avoid problems caused by that, + // we use the following ScopedAXEmbeddedObjectBehaviorSetter to modify the + // value of the global variable to what is really expected on UIA. + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior( + AXEmbeddedObjectBehavior::kSuppressCharacter); std::u16string search_string = base::WideToUTF16(string); if (search_string.length() <= 0) @@ -1108,9 +1130,7 @@ AXPlatformNodeWin* AXPlatformNodeTextRangeProviderWin::GetOwner() const { const AXNode* anchor = position->GetAnchor(); DCHECK(anchor); - AXTreeID tree_id = anchor->tree()->GetAXTreeID(); - const AXTreeManager* ax_tree_manager = - AXTreeManagerMap::GetInstance().GetManager(tree_id); + const AXTreeManager* ax_tree_manager = position->GetManager(); DCHECK(ax_tree_manager); const AXPlatformTreeManager* platform_tree_manager = @@ -1359,8 +1379,7 @@ void AXPlatformNodeTextRangeProviderWin::NormalizeAsUnignoredTextRange( AXPlatformNodeDelegate* AXPlatformNodeTextRangeProviderWin::GetRootDelegate( const ui::AXTreeID tree_id) { - const AXTreeManager* ax_tree_manager = - AXTreeManagerMap::GetInstance().GetManager(tree_id); + const AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id); DCHECK(ax_tree_manager); AXNode* root_node = ax_tree_manager->GetRootAsAXNode(); const AXPlatformNode* root_platform_node = @@ -1595,18 +1614,16 @@ void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::SetEnd( void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::AddObserver( const AXTreeID tree_id) { - AXTreeManager* ax_tree_manager = - AXTreeManagerMap::GetInstance().GetManager(tree_id); + AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id); DCHECK(ax_tree_manager); - ax_tree_manager->AddObserver(this); + ax_tree_manager->ax_tree()->AddObserver(this); } void AXPlatformNodeTextRangeProviderWin::TextRangeEndpoints::RemoveObserver( const AXTreeID tree_id) { - AXTreeManager* ax_tree_manager = - AXTreeManagerMap::GetInstance().GetManager(tree_id); + AXTreeManager* ax_tree_manager = AXTreeManager::FromID(tree_id); if (ax_tree_manager) - ax_tree_manager->RemoveObserver(this); + ax_tree_manager->ax_tree()->RemoveObserver(this); } // Ensures that our endpoints are located on non-deleted nodes (step 1, case A diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_fuzzer.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_fuzzer.cc new file mode 100644 index 00000000000..0ebe9747757 --- /dev/null +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_fuzzer.cc @@ -0,0 +1,245 @@ +// Copyright 2022 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 "base/win/atl.h" // Must be before UIAutomationCore.h + +#include <UIAutomationClient.h> +#include <UIAutomationCore.h> +#include <UIAutomationCoreApi.h> + +#include <memory> +#include <tuple> +#include <utility> + +#include "base/at_exit.h" +#include "base/i18n/icu_util.h" +#include "base/test/scoped_feature_list.h" +#include "base/win/atl.h" +#include "base/win/scoped_bstr.h" +#include "base/win/scoped_safearray.h" +#include "base/win/scoped_variant.h" +#include "ui/accessibility/ax_enums.mojom.h" +#include "ui/accessibility/ax_node.h" +#include "ui/accessibility/ax_node_data.h" +#include "ui/accessibility/ax_node_position.h" +#include "ui/accessibility/ax_position.h" +#include "ui/accessibility/ax_range.h" +#include "ui/accessibility/ax_role_properties.h" +#include "ui/accessibility/ax_tree.h" +#include "ui/accessibility/ax_tree_data.h" +#include "ui/accessibility/ax_tree_fuzzer_util.h" +#include "ui/accessibility/ax_tree_id.h" +#include "ui/accessibility/ax_tree_update.h" +#include "ui/accessibility/platform/ax_fragment_root_delegate_win.h" +#include "ui/accessibility/platform/ax_fragment_root_win.h" +#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h" +#include "ui/accessibility/platform/test_ax_node_wrapper.h" +#include "ui/accessibility/test_ax_tree_manager.h" + +using Microsoft::WRL::ComPtr; + +// We generate positions using fuzz data, this constant should be aligned +// with the amount of bytes needed to generate a new text range. +constexpr size_t kBytesNeededToGenerateTextRange = 4; +// This should be aligned with the amount of bytes needed to mutate a text range +// in ...MutateTextRangeProvider... +constexpr size_t kBytesNeededToMutateTextRange = 4; + +// Min/Max node size for the initial tree. +constexpr size_t kMinNodeCount = 10; +constexpr size_t kMaxNodeCount = kMinNodeCount + 50; + +// Min fuzz data needed for fuzzer to function. +// We need to ensure we have enough data to create a tree with text, as well as +// generate a couple of text ranges and mutate them. +constexpr size_t kMinFuzzDataSize = + kMinNodeCount * AXTreeFuzzerGenerator::kMinimumNewNodeFuzzDataSize + + kMinNodeCount * AXTreeFuzzerGenerator::kMinTextFuzzDataSize + + 2 * kBytesNeededToGenerateTextRange + 2 * kBytesNeededToMutateTextRange; + +// Cap fuzz data to avoid slowness. +constexpr size_t kMaxFuzzDataSize = 20000; + +ui::AXPlatformNode* AXPlatformNodeFromNode(ui::AXTree* tree, ui::AXNode* node) { + const ui::TestAXNodeWrapper* wrapper = + ui::TestAXNodeWrapper::GetOrCreate(tree, node); + return wrapper ? wrapper->ax_platform_node() : nullptr; +} + +template <typename T> +ComPtr<T> QueryInterfaceFromNode(ui::AXTree* tree, ui::AXNode* node) { + ui::AXPlatformNode* ax_platform_node = AXPlatformNodeFromNode(tree, node); + if (!ax_platform_node) + return ComPtr<T>(); + ComPtr<T> result; + ax_platform_node->GetNativeViewAccessible()->QueryInterface(__uuidof(T), + &result); + return result; +} + +// This method returns a text range scoped to this node. +void GetTextRangeProviderFromTextNode( + ui::AXTree* tree, + ui::AXNode* text_node, + ComPtr<ITextRangeProvider>& text_range_provider) { + ComPtr<IRawElementProviderSimple> provider_simple = + QueryInterfaceFromNode<IRawElementProviderSimple>(tree, text_node); + if (!provider_simple.Get()) + return; + ComPtr<ITextProvider> text_provider; + provider_simple->GetPatternProvider(UIA_TextPatternId, &text_provider); + + if (!text_provider.Get()) + return; + + text_provider->get_DocumentRange(&text_range_provider); + + ComPtr<ui::AXPlatformNodeTextRangeProviderWin> text_range_provider_interal; + text_range_provider->QueryInterface( + IID_PPV_ARGS(&text_range_provider_interal)); + ui::AXPlatformNode* ax_platform_node = + AXPlatformNodeFromNode(tree, text_node); + text_range_provider_interal->SetOwnerForTesting( + static_cast<ui::AXPlatformNodeWin*>(ax_platform_node)); +} + +void CallComparisonAPIs(const ComPtr<ITextRangeProvider>& text_range, + const ComPtr<ITextRangeProvider>& other_text_range) { + BOOL are_same; + std::ignore = text_range->Compare(other_text_range.Get(), &are_same); + int compare_endpoints_result; + std::ignore = text_range->CompareEndpoints( + TextPatternRangeEndpoint_Start, other_text_range.Get(), + TextPatternRangeEndpoint_Start, &compare_endpoints_result); + std::ignore = text_range->CompareEndpoints( + TextPatternRangeEndpoint_End, other_text_range.Get(), + TextPatternRangeEndpoint_Start, &compare_endpoints_result); + std::ignore = text_range->CompareEndpoints( + TextPatternRangeEndpoint_Start, other_text_range.Get(), + TextPatternRangeEndpoint_End, &compare_endpoints_result); + std::ignore = text_range->CompareEndpoints( + TextPatternRangeEndpoint_End, other_text_range.Get(), + TextPatternRangeEndpoint_End, &compare_endpoints_result); +} + +TextPatternRangeEndpoint GenerateEndpoint(unsigned char byte) { + return (byte % 2) ? TextPatternRangeEndpoint_Start + : TextPatternRangeEndpoint_End; +} + +TextUnit GenerateTextUnit(unsigned char byte) { + return static_cast<TextUnit>(byte % 7); +} + +enum class TextRangeMutation { + kMoveEndpointByRange, + kExpandToEnclosingUnit, + kMove, + kMoveEndpointByUnit, + kLast +}; + +TextRangeMutation GenerateTextRangeMutation(unsigned char byte) { + constexpr unsigned char max = + static_cast<unsigned char>(TextRangeMutation::kLast); + return static_cast<TextRangeMutation>(byte % max); +} + +void MutateTextRangeProvider(ComPtr<ITextRangeProvider>& text_range, + const ComPtr<ITextRangeProvider>& other_text_range, + FuzzerData& fuzz_data) { + const int kMaxMoveCount = 20; + TextUnit unit = GenerateTextUnit(fuzz_data.NextByte()); + int units_moved; + + TextRangeMutation mutation_type = + GenerateTextRangeMutation(fuzz_data.NextByte()); + switch (mutation_type) { + case TextRangeMutation::kMoveEndpointByRange: + if (other_text_range.Get()) { + text_range->MoveEndpointByRange(GenerateEndpoint(fuzz_data.NextByte()), + other_text_range.Get(), + GenerateEndpoint(fuzz_data.NextByte())); + return; + } + ABSL_FALLTHROUGH_INTENDED; + case TextRangeMutation::kExpandToEnclosingUnit: + text_range->ExpandToEnclosingUnit(unit); + return; + case TextRangeMutation::kMove: + text_range->Move(unit, fuzz_data.NextByte() % kMaxMoveCount, + &units_moved); + return; + case TextRangeMutation::kMoveEndpointByUnit: + text_range->MoveEndpointByUnit(GenerateEndpoint(fuzz_data.NextByte()), + unit, fuzz_data.NextByte() % kMaxMoveCount, + &units_moved); + return; + case TextRangeMutation::kLast: + NOTREACHED(); + } +} + +struct Environment { + Environment() { CHECK(base::i18n::InitializeICU()); } + base::AtExitManager at_exit_manager; +}; + +// Entry point for LibFuzzer. +extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data, size_t size) { + if (size < kMinFuzzDataSize || size > kMaxFuzzDataSize) + return 0; + static Environment env; + + FuzzerData fuzz_data(data, size); + AXTreeFuzzerGenerator generator; + + // Create initial tree. + const size_t node_count = + kMinNodeCount + fuzz_data.NextByte() % (kMaxNodeCount - kMinNodeCount); + generator.GenerateInitialUpdate(fuzz_data, node_count); + + ui::AXTree* tree = generator.GetTree(); + + // Run with --v=1 to aid in debugging a specific crash. + VLOG(1) << tree->ToString(); + + // Loop until data is expended. + std::vector<ComPtr<ITextRangeProvider>> created_ranges; + while (fuzz_data.RemainingBytes() > kBytesNeededToGenerateTextRange) { + // Create a new position on a random node in the tree. + { + // To ensure that anchor_id is between |ui::kInvalidAXNodeID| and the max + // ID of the tree (non-inclusive), get a number [0, max_id - 1) and then + // shift by 1 to get [1, max_id) + ui::AXNodeID anchor_id = + (fuzz_data.NextByte() % (generator.GetMaxAssignedID() - 1)) + 1; + ui::AXNode* anchor = tree->GetFromId(anchor_id); + if (!anchor) + continue; + ComPtr<ITextRangeProvider> text_range_provider; + GetTextRangeProviderFromTextNode(tree, anchor, text_range_provider); + if (text_range_provider) + created_ranges.push_back(std::move(text_range_provider)); + } + for (size_t i = 0; i < created_ranges.size(); ++i) { + ComPtr<ITextRangeProvider> text_range = created_ranges[i]; + ComPtr<ITextRangeProvider> other_range; + if (i > 0) + other_range = created_ranges[i - 1].Get(); + if (!other_range.Get()) + continue; + + CallComparisonAPIs(text_range.Get(), other_range); + if (fuzz_data.RemainingBytes() > kBytesNeededToMutateTextRange) + MutateTextRangeProvider(text_range, other_range, fuzz_data); + } + // Do a Tree Update. + if (!generator.GenerateTreeUpdate(fuzz_data, 5)) + break; + } + tree->Destroy(); + ui::TestAXNodeWrapper::ResetGlobalState(); + return 0; +} diff --git a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc index ceee20b402c..8ff0bf08649 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_textrangeprovider_win_unittest.cc @@ -444,6 +444,8 @@ class AXPlatformNodeTextRangeProviderTest : public ui::AXPlatformNodeWinTest { text_field.AddState(ax::mojom::State::kEditable); text_field.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + text_field.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); text_field.SetValue(ALL_TEXT); text_field.AddIntListAttribute(ax::mojom::IntListAttribute::kLineStarts, std::vector<int32_t>{0, 7}); @@ -2993,6 +2995,8 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, text_input_data.AddState(ax::mojom::State::kEditable); text_input_data.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + text_input_data.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); ui::AXNodeData group2_data; group2_data.id = 5; @@ -3015,7 +3019,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, empty_text_data.role = ax::mojom::Role::kStaticText; empty_text_data.AddState(ax::mojom::State::kEditable); text_content = ""; - empty_text_data.SetName(text_content); + empty_text_data.SetNameExplicitlyEmpty(); ComputeWordBoundariesOffsets(text_content, word_start_offsets, word_end_offsets); empty_text_data.AddIntListAttribute(ax::mojom::IntListAttribute::kWordStarts, @@ -3376,6 +3380,8 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, search_box.role = ax::mojom::Role::kSearchBox; search_box.AddState(ax::mojom::State::kEditable); search_box.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + search_box.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "search"); paragraph_data.child_ids.push_back(search_box.id); ui::AXNodeData search_text; @@ -3531,6 +3537,124 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, } TEST_F(AXPlatformNodeTextRangeProviderTest, + TestITextRangeProviderGetEnclosingElementRichButton) { + // Set up ax tree with the following structure: + // + // root + // ++button_1 + // ++++static_text_1 + // ++++++inline_text_1 + // ++button_2 + // ++++heading + // ++++++statix_text_2 + // ++++++++inline_text_2 + + ui::AXNodeData root; + ui::AXNodeData button_1; + ui::AXNodeData static_text_1; + ui::AXNodeData inline_text_1; + ui::AXNodeData button_2; + ui::AXNodeData heading; + ui::AXNodeData static_text_2; + ui::AXNodeData inline_text_2; + + root.id = 1; + button_1.id = 2; + static_text_1.id = 3; + inline_text_1.id = 4; + button_2.id = 5; + heading.id = 6; + static_text_2.id = 7; + inline_text_2.id = 8; + + root.role = ax::mojom::Role::kRootWebArea; + root.child_ids = {button_1.id, button_2.id}; + + button_1.role = ax::mojom::Role::kButton; + button_1.child_ids.push_back(static_text_1.id); + + static_text_1.role = ax::mojom::Role::kStaticText; + static_text_1.child_ids.push_back(inline_text_1.id); + + inline_text_1.role = ax::mojom::Role::kInlineTextBox; + + button_2.role = ax::mojom::Role::kButton; + button_2.child_ids.push_back(heading.id); + + heading.role = ax::mojom::Role::kHeading; + heading.child_ids.push_back(static_text_2.id); + + static_text_2.role = ax::mojom::Role::kStaticText; + static_text_2.child_ids.push_back(inline_text_2.id); + + inline_text_2.role = ax::mojom::Role::kInlineTextBox; + + ui::AXTreeUpdate update; + ui::AXTreeData tree_data; + tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root.id; + update.nodes = {root, button_1, static_text_1, inline_text_1, + button_2, heading, static_text_2, inline_text_2}; + Init(update); + + // Set up variables from the tree for testing. + AXNode* button_1_node = GetRootAsAXNode()->children()[0]; + AXNode* static_text_1_node = button_1_node->children()[0]; + AXNode* inline_text_1_node = static_text_1_node->children()[0]; + AXNode* button_2_node = GetRootAsAXNode()->children()[1]; + AXNode* heading_node = button_2_node->children()[0]; + AXNode* static_text_2_node = heading_node->children()[0]; + AXNode* inline_text_2_node = static_text_2_node->children()[0]; + AXPlatformNodeWin* owner = + static_cast<AXPlatformNodeWin*>(AXPlatformNodeFromNode(button_1_node)); + ASSERT_NE(owner, nullptr); + + ComPtr<IRawElementProviderSimple> button_1_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(button_1_node); + ComPtr<IRawElementProviderSimple> inline_text_1_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(inline_text_1_node); + + ComPtr<IRawElementProviderSimple> static_text_2_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(static_text_2_node); + ComPtr<IRawElementProviderSimple> inline_text_2_node_raw = + QueryInterfaceFromNode<IRawElementProviderSimple>(inline_text_2_node); + + // 1. The first button should hide its children since it contains a single + // text node. Thus, calling GetEnclosingElement on a descendant inline text + // box should return the button itself. + ComPtr<ITextProvider> text_provider; + EXPECT_HRESULT_SUCCEEDED(inline_text_1_node_raw->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + ComPtr<ITextRangeProvider> text_range_provider; + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + ComPtr<IRawElementProviderSimple> enclosing_element; + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(button_1_node_raw.Get(), enclosing_element.Get()); + + // 2. The second button shouldn't hide its children since it doesn't contain a + // single text node (it contains a heading node). Thus, calling + // GetEnclosingElement on a descendant inline text box should return the + // parent node. + EXPECT_HRESULT_SUCCEEDED(inline_text_2_node_raw->GetPatternProvider( + UIA_TextPatternId, &text_provider)); + + EXPECT_HRESULT_SUCCEEDED( + text_provider->get_DocumentRange(&text_range_provider)); + SetOwner(owner, text_range_provider.Get()); + + EXPECT_HRESULT_SUCCEEDED( + text_range_provider->GetEnclosingElement(&enclosing_element)); + EXPECT_EQ(static_text_2_node_raw.Get(), enclosing_element.Get()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderMoveEndpointByRange) { Init(BuildTextDocument({"some text", "more text"})); @@ -3903,6 +4027,8 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, input_text_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, 0xFFADC0DEU); input_text_data.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + input_text_data.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); input_text_data.SetName("placeholder"); input_text_data.child_ids = {13}; @@ -3928,6 +4054,8 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, 0xFFADC0DEU); input_text_data2.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + input_text_data2.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); input_text_data2.SetName("foo"); input_text_data2.child_ids = {15}; @@ -5112,6 +5240,92 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderFindText) { } TEST_F(AXPlatformNodeTextRangeProviderTest, + FindTextWithEmbeddedObjectCharacter) { + // ++1 kRootWebArea + // ++++2 kList + // ++++++3 kListItem + // ++++++++4 kStaticText + // ++++++++++5 kInlineTextBox + // ++++++6 kListItem + // ++++++++7 kStaticText + // ++++++++++8 kInlineTextBox + ui::AXNodeData root_1; + ui::AXNodeData list_2; + ui::AXNodeData list_item_3; + ui::AXNodeData static_text_4; + ui::AXNodeData inline_box_5; + ui::AXNodeData list_item_6; + ui::AXNodeData static_text_7; + ui::AXNodeData inline_box_8; + + root_1.id = 1; + list_2.id = 2; + list_item_3.id = 3; + static_text_4.id = 4; + inline_box_5.id = 5; + list_item_6.id = 6; + static_text_7.id = 7; + inline_box_8.id = 8; + + root_1.role = ax::mojom::Role::kRootWebArea; + root_1.child_ids = {list_2.id}; + + list_2.role = ax::mojom::Role::kList; + list_2.child_ids = {list_item_3.id, list_item_6.id}; + + list_item_3.role = ax::mojom::Role::kListItem; + list_item_3.child_ids = {static_text_4.id}; + + static_text_4.role = ax::mojom::Role::kStaticText; + static_text_4.SetName("foo"); + static_text_4.child_ids = {inline_box_5.id}; + + inline_box_5.role = ax::mojom::Role::kInlineTextBox; + inline_box_5.SetName("foo"); + + list_item_6.role = ax::mojom::Role::kListItem; + list_item_6.child_ids = {static_text_7.id}; + + static_text_7.role = ax::mojom::Role::kStaticText; + static_text_7.child_ids = {inline_box_8.id}; + static_text_7.SetName("bar"); + + inline_box_8.role = ax::mojom::Role::kInlineTextBox; + inline_box_8.SetName("bar"); + + ui::AXTreeUpdate update; + ui::AXTreeData tree_data; + tree_data.tree_id = ui::AXTreeID::CreateNewAXTreeID(); + update.tree_data = tree_data; + update.has_tree_data = true; + update.root_id = root_1.id; + update.nodes = {root_1, list_2, list_item_3, static_text_4, + inline_box_5, list_item_6, static_text_7, inline_box_8}; + + Init(update); + + AXNode* root_node = GetRootAsAXNode(); + ComPtr<ITextRangeProvider> text_range_provider; + GetTextRangeProviderFromTextNode(text_range_provider, root_node); + + base::win::ScopedBstr find_string(L"oobar"); + Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found; + EXPECT_HRESULT_SUCCEEDED(text_range_provider->FindText(find_string.Get(), + false, false, &text_range_provider_found)); + ASSERT_TRUE(text_range_provider_found.Get()); + Microsoft::WRL::ComPtr<AXPlatformNodeTextRangeProviderWin> + text_range_provider_win; + text_range_provider_found->QueryInterface( + IID_PPV_ARGS(&text_range_provider_win)); + ASSERT_TRUE(GetStart(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(5, GetStart(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(1, GetStart(text_range_provider_win.Get())->text_offset()); + ASSERT_TRUE(GetEnd(text_range_provider_win.Get())->IsTextPosition()); + EXPECT_EQ(8, GetEnd(text_range_provider_win.Get())->anchor_id()); + EXPECT_EQ(3, GetEnd(text_range_provider_win.Get())->text_offset()); +} + +TEST_F(AXPlatformNodeTextRangeProviderTest, TestITextRangeProviderFindTextBackwards) { Init(BuildTextDocument({"text", "some", "text"}, false /* build_word_boundaries_offsets */, @@ -6294,7 +6508,7 @@ TEST_F(AXPlatformNodeTextRangeProviderTest, TestValidateStartAndEnd) { // Now modify the tree so that start_ is pointing to a node that has been // removed from the tree. - text_data.SetName(""); + text_data.SetNameExplicitlyEmpty(); AXTreeUpdate test_update2; test_update2.nodes = {text_data}; ASSERT_TRUE(GetTree()->Unserialize(test_update2)); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc index 9d8c8f1fd06..d955e088225 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.cc @@ -7,6 +7,7 @@ #include "ui/accessibility/ax_constants.mojom.h" #include "ui/accessibility/platform/ax_platform_node_base.h" #include "ui/accessibility/platform/test_ax_node_wrapper.h" +#include "ui/accessibility/platform/test_ax_tree_update.h" namespace ui { @@ -74,6 +75,12 @@ void AXPlatformNodeTest::Init( Init(update); } +AXTree* AXPlatformNodeTest::Init(const TestAXTreeUpdateNode& root) { + TestAXTreeUpdate update(root); + Init(update); + return GetTree(); +} + AXTreeUpdate AXPlatformNodeTest::BuildTextField() { AXNodeData text_field_node; text_field_node.id = 1; @@ -81,6 +88,8 @@ AXTreeUpdate AXPlatformNodeTest::BuildTextField() { text_field_node.AddState(ax::mojom::State::kEditable); text_field_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + text_field_node.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); text_field_node.SetValue("How now brown cow."); AXTreeUpdate update; @@ -98,6 +107,8 @@ AXTreeUpdate AXPlatformNodeTest::BuildTextFieldWithSelectionRange( text_field_node.AddState(ax::mojom::State::kEditable); text_field_node.AddStringAttribute(ax::mojom::StringAttribute::kHtmlTag, "input"); + text_field_node.AddStringAttribute(ax::mojom::StringAttribute::kInputType, + "text"); text_field_node.AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, true); text_field_node.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, start); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_unittest.h b/chromium/ui/accessibility/platform/ax_platform_node_unittest.h index b006162bd24..a795d986e1a 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_unittest.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_unittest.h @@ -15,6 +15,8 @@ namespace ui { +struct TestAXTreeUpdateNode; + class AXPlatformNodeTest : public ::testing::Test, public TestAXTreeManager { public: AXPlatformNodeTest(); @@ -42,6 +44,9 @@ class AXPlatformNodeTest : public ::testing::Test, public TestAXTreeManager { const AXNodeData& node11 = AXNodeData(), const AXNodeData& node12 = AXNodeData()); + // Initialize given an AXTreeUpdate by given TestAXTreeUpdateNode instance. + AXTree* Init(const TestAXTreeUpdateNode& root); + AXTreeUpdate BuildTextField(); AXTreeUpdate BuildTextFieldWithSelectionRange(int32_t start, int32_t stop); AXTreeUpdate BuildContentEditable(); diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win.cc b/chromium/ui/accessibility/platform/ax_platform_node_win.cc index 7daf94358ff..36f633b07ac 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win.cc @@ -11,10 +11,10 @@ #include <map> #include <set> #include <string> -#include <unordered_set> #include <utility> #include <vector> +#include "base/containers/flat_set.h" #include "base/json/json_writer.h" #include "base/lazy_instance.h" #include "base/metrics/histogram_functions.h" @@ -40,6 +40,7 @@ #include "ui/accessibility/ax_action_handler_registry.h" #include "ui/accessibility/ax_active_popup.h" #include "ui/accessibility/ax_constants.mojom.h" +#include "ui/accessibility/ax_enum_localization_util.h" #include "ui/accessibility/ax_enum_util.h" #include "ui/accessibility/ax_mode_observer.h" #include "ui/accessibility/ax_node_data.h" @@ -208,7 +209,7 @@ namespace ui { namespace { -typedef std::unordered_set<AXPlatformNodeWin*> AXPlatformNodeWinSet; +typedef base::flat_set<AXPlatformNodeWin*> AXPlatformNodeWinSet; // Set of all AXPlatformNodeWin objects that were the target of an // alert event. base::LazyInstance<AXPlatformNodeWinSet>::Leaky g_alert_targets = @@ -471,6 +472,18 @@ SAFEARRAY* AXPlatformNodeWin::CreateUIAControllerForArray() { platform_node_list.push_back(view_popup_node_win); } + // The aria-errormessage attribute (mapped to the kErrormessageId) is expected + // to be exposed through the ControllerFor property on UIA: + // https://www.w3.org/TR/wai-aria-1.1/#aria-errormessage. + if (HasIntAttribute(ax::mojom::IntAttribute::kErrormessageId)) { + AXPlatformNodeWin* error_message_node_win = + static_cast<AXPlatformNodeWin*>(GetFromUniqueId( + GetIntAttribute(ax::mojom::IntAttribute::kErrormessageId))); + + if (IsValidUiaRelationTarget(error_message_node_win)) + platform_node_list.push_back(error_message_node_win); + } + return CreateUIAElementsSafeArray(platform_node_list); } @@ -761,7 +774,7 @@ AXPlatformNodeWin::UIARoleProperties AXPlatformNodeWin::GetUIARoleProperties() { // If this is a web area for a presentational iframe, give it a role of // something other than document so that the fact that it's a separate doc // is not exposed to AT. - if (GetDelegate()->IsWebAreaForPresentationalIframe()) + if (GetDelegate()->IsRootWebAreaForPresentationalIframe()) return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId, L"group"}; // See UIARoleProperties for descriptions of the properties. @@ -1010,8 +1023,8 @@ AXPlatformNodeWin::UIARoleProperties AXPlatformNodeWin::GetUIARoleProperties() { L"document"}; case ax::mojom::Role::kGraphicsObject: - return {UIALocalizationStrategy::kSupply, UIA_PaneControlTypeId, - L"region"}; + return {UIALocalizationStrategy::kSupply, UIA_GroupControlTypeId, + L"group"}; case ax::mojom::Role::kGraphicsSymbol: return {UIALocalizationStrategy::kSupply, UIA_ImageControlTypeId, L"img"}; @@ -1214,7 +1227,7 @@ AXPlatformNodeWin::UIARoleProperties AXPlatformNodeWin::GetUIARoleProperties() { L"document"}; case ax::mojom::Role::kPopUpButton: { - const std::string html_tag = + const std::string& html_tag = GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); if (html_tag == "select") { return {UIALocalizationStrategy::kDeferToControlType, @@ -4144,7 +4157,7 @@ IFACEMETHODIMP AXPlatformNodeWin::get_caretOffset(LONG* offset) { AXPlatformNode::NotifyAddAXModeFlags(kScreenReaderAndHTMLAccessibilityModes); *offset = 0; - if (!HasCaret()) + if (!HasVisibleCaretOrSelection()) return S_FALSE; int selection_start, selection_end; @@ -4561,9 +4574,11 @@ IFACEMETHODIMP AXPlatformNodeWin::setSelections(LONG nSelections, return E_INVALIDARG; AXPosition start_position = - start_node->HypertextOffsetToEndpoint(selections->startOffset); + start_node->HypertextOffsetToEndpoint(selections->startOffset) + ->AsDomSelectionPosition(); AXPosition end_position = - end_node->HypertextOffsetToEndpoint(selections->endOffset); + end_node->HypertextOffsetToEndpoint(selections->endOffset) + ->AsDomSelectionPosition(); if (!start_position->IsNullPosition() || end_position->IsNullPosition()) return E_INVALIDARG; @@ -5223,7 +5238,7 @@ HRESULT AXPlatformNodeWin::GetPropertyValueImpl(PROPERTYID property_id, // Localized Control Type of "output" whereas the Core-AAM states // the Localized Control Type of the ARIA status role should be // "status". - const std::string html_tag = + const std::string& html_tag = GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag); std::u16string localized_control_type = html_tag == "output" @@ -6110,7 +6125,7 @@ int AXPlatformNodeWin::MSAARole() { // If this is a web area for a presentational iframe, give it a role of // something other than DOCUMENT so that the fact that it's a separate doc // is not exposed to AT. - if (GetDelegate()->IsWebAreaForPresentationalIframe()) + if (GetDelegate()->IsRootWebAreaForPresentationalIframe()) return ROLE_SYSTEM_GROUPING; switch (GetRole()) { @@ -6707,7 +6722,7 @@ int32_t AXPlatformNodeWin::ComputeIA2Role() { // If this is a web area for a presentational iframe, give it a role of // something other than DOCUMENT so that the fact that it's a separate doc // is not exposed to AT. - if (GetDelegate()->IsWebAreaForPresentationalIframe()) { + if (GetDelegate()->IsRootWebAreaForPresentationalIframe()) { return ROLE_SYSTEM_GROUPING; } @@ -7273,6 +7288,10 @@ bool AXPlatformNodeWin::IsUIAControl() const { case ax::mojom::Role::kLabelText: case ax::mojom::Role::kListBoxOption: case ax::mojom::Role::kListItem: + // Treat the root of a MathML tree as content/control so that it is seen + // by UIA clients. The remainder of the tree remains as text for now until + // UIA mappings for MathML are defined (https://crbug.com/1260585). + case ax::mojom::Role::kMathMLMath: case ax::mojom::Role::kMeter: case ax::mojom::Role::kProgressIndicator: case ax::mojom::Role::kRow: @@ -7362,20 +7381,15 @@ bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { return true; auto role = GetRole(); - if (HasPresentationalChildren(role)) - return true; - switch (role) { - // Other elements that are expected by UIA to hide their children without - // having "Children Presentational: True". - // + // Even though a node with role kButton has presentational children, it + // should only hide its children from UIA when it has a single text node + // (to avoid having its name announced twice). This is because buttons can + // have complex structures and they shouldn't hide their subtree. + case ax::mojom::Role::kButton: // TODO(bebeaudr): We might be able to remove ax::mojom::Role::kLink once // http://crbug.com/1054514 is fixed. Links should not have to hide their // children. - // TODO(virens): |kPdfActionableHighlight| needs to follow a fix similar to - // links. At present Pdf highlghts have text nodes as children. But, we may - // enable pdf highlights to have complex children like links based on user - // feedback. case ax::mojom::Role::kLink: // Links with a single text-only child should hide their subtree. if (GetChildCount() == 1) { @@ -7383,10 +7397,16 @@ bool AXPlatformNodeWin::ShouldHideChildrenForUIA() const { return only_child && only_child->IsText(); } return false; + // TODO(virens): |kPdfActionableHighlight| needs to follow a fix similar to + // links. At present Pdf highlights have text nodes as children. But, we may + // enable pdf highlights to have complex children like links based on user + // feedback. case ax::mojom::Role::kPdfActionableHighlight: return true; default: - return false; + // UIA expects nodes that have "Children Presentational: True" to hide + // their children. + return HasPresentationalChildren(role); } } @@ -7424,9 +7444,9 @@ int AXPlatformNodeWin::MSAAState() const { // Exposing the busy state on the root web area means the NVDA user will end // up without a virtualBuffer until the page fully loads. So if we have // content, don't expose the busy state. - if (GetBoolAttribute(ax::mojom::BoolAttribute::kBusy) && - !IsPlatformDocumentWithContent()) { - msaa_state |= STATE_SYSTEM_BUSY; + if (GetBoolAttribute(ax::mojom::BoolAttribute::kBusy)) { + if (!IsPlatformDocument() || !GetChildCount()) + msaa_state |= STATE_SYSTEM_BUSY; } if (HasState(ax::mojom::State::kCollapsed)) diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc index 7e75543ab52..9c9dfb7f070 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.cc @@ -3485,19 +3485,6 @@ TEST_F(AXPlatformNodeWinTest, IAccessibleTextTextFieldGetCaretOffsetNoCaret) { EXPECT_EQ(0, offset); } -TEST_F(AXPlatformNodeWinTest, IAccessibleTextTextFieldGetCaretOffsetHasCaret) { - Init(BuildTextFieldWithSelectionRange(1, 2)); - - ComPtr<IAccessible2> ia2_text_field = ToIAccessible2(GetRootIAccessible()); - ComPtr<IAccessibleText> text_field; - ia2_text_field.As(&text_field); - ASSERT_NE(nullptr, text_field.Get()); - - LONG offset; - EXPECT_HRESULT_SUCCEEDED(text_field->get_caretOffset(&offset)); - EXPECT_EQ(2, offset); -} - TEST_F(AXPlatformNodeWinTest, IAccessibleTextContextEditableGetCaretOffsetNoCaret) { Init(BuildContentEditable()); @@ -4387,7 +4374,7 @@ TEST_F(AXPlatformNodeWinTest, UIAGetControllerForPropertyId) { AXNodeData root; root.id = 1; root.role = ax::mojom::Role::kRootWebArea; - root.child_ids = {2, 3, 4}; + root.child_ids = {2, 3, 4, 5, 6}; AXNodeData tab; tab.id = 2; @@ -4407,7 +4394,17 @@ TEST_F(AXPlatformNodeWinTest, UIAGetControllerForPropertyId) { panel2.role = ax::mojom::Role::kTabPanel; panel2.SetName("panel2"); - Init(root, tab, panel1, panel2); + AXNodeData group1; + group1.id = 5; + group1.role = ax::mojom::Role::kGenericContainer; + group1.AddIntAttribute(ax::mojom::IntAttribute::kErrormessageId, 6); + + AXNodeData text1; + text1.id = 6; + text1.role = ax::mojom::Role::kStaticText; + text1.SetName("text1"); + + Init(root, tab, panel1, panel2, group1, text1); TestAXNodeWrapper* root_wrapper = TestAXNodeWrapper::GetOrCreate(GetTree(), GetRootAsAXNode()); root_wrapper->BuildAllWrappers(GetTree(), GetRootAsAXNode()); @@ -4429,6 +4426,17 @@ TEST_F(AXPlatformNodeWinTest, UIAGetControllerForPropertyId) { EXPECT_UIA_PROPERTY_ELEMENT_ARRAY_BSTR_EQ( tab_node, UIA_ControllerForPropertyId, UIA_NamePropertyId, expected_names_2); + + // The aria-errormessage attribute should be exposed through the ControllerFor + // UIA property. + ComPtr<IRawElementProviderSimple> group1_node = + QueryInterfaceFromNode<IRawElementProviderSimple>( + GetRootAsAXNode()->children()[3]); + + std::vector<std::wstring> expected_names_3 = {L"text1"}; + EXPECT_UIA_PROPERTY_ELEMENT_ARRAY_BSTR_EQ( + group1_node, UIA_ControllerForPropertyId, UIA_NamePropertyId, + expected_names_3); } TEST_F(AXPlatformNodeWinTest, UIAGetDescribedByPropertyId) { @@ -5785,7 +5793,12 @@ TEST_F(AXPlatformNodeWinTest, ComputeUIAControlType) { child6.role = ax::mojom::Role::kDialog; root.child_ids.push_back(child6.id); - Init(root, child1, child2, child3, child4, child5, child6); + AXNodeData child7; + child7.id = 8; + child7.role = ax::mojom::Role::kGraphicsObject; + root.child_ids.push_back(child7.id); + + Init(root, child1, child2, child3, child4, child5, child6, child7); EXPECT_UIA_INT_EQ( QueryInterfaceFromNodeId<IRawElementProviderSimple>(child1.id), @@ -5805,6 +5818,9 @@ TEST_F(AXPlatformNodeWinTest, ComputeUIAControlType) { EXPECT_UIA_INT_EQ( QueryInterfaceFromNodeId<IRawElementProviderSimple>(child6.id), UIA_ControlTypePropertyId, int{UIA_WindowControlTypeId}); + EXPECT_UIA_INT_EQ( + QueryInterfaceFromNodeId<IRawElementProviderSimple>(child7.id), + UIA_ControlTypePropertyId, int{UIA_GroupControlTypeId}); } TEST_F(AXPlatformNodeWinTest, UIALandmarkType) { diff --git a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h index 2376d6bb2f8..085d6b3d55d 100644 --- a/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h +++ b/chromium/ui/accessibility/platform/ax_platform_node_win_unittest.h @@ -134,7 +134,7 @@ class AXPlatformNodeWinTest : public AXPlatformNodeTest { std::unique_ptr<AXFragmentRootWin> ax_fragment_root_; std::unique_ptr<TestFragmentRootDelegate> test_fragment_root_delegate_; - testing::ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior_; + ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior_; base::test::ScopedFeatureList scoped_feature_list_; }; diff --git a/chromium/ui/accessibility/platform/ax_platform_tree_manager.h b/chromium/ui/accessibility/platform/ax_platform_tree_manager.h index 8d2944a433d..642104fa655 100644 --- a/chromium/ui/accessibility/platform/ax_platform_tree_manager.h +++ b/chromium/ui/accessibility/platform/ax_platform_tree_manager.h @@ -25,6 +25,11 @@ class AX_EXPORT AXPlatformTreeManager : public AXTreeManager { // Returns an AXPlatformNode that corresponds to the given |node|. virtual AXPlatformNode* GetPlatformNodeFromTree(const AXNode& node) const = 0; + + protected: + explicit AXPlatformTreeManager(const AXTreeID& tree_id, + std::unique_ptr<AXTree> tree) + : AXTreeManager(tree_id, std::move(tree)) {} }; } // namespace ui diff --git a/chromium/ui/accessibility/platform/ax_unique_id.cc b/chromium/ui/accessibility/platform/ax_unique_id.cc index cfb433a1280..08353303bf6 100644 --- a/chromium/ui/accessibility/platform/ax_unique_id.cc +++ b/chromium/ui/accessibility/platform/ax_unique_id.cc @@ -5,9 +5,9 @@ #include "ui/accessibility/platform/ax_unique_id.h" #include <memory> -#include <unordered_set> #include "base/containers/contains.h" +#include "base/containers/flat_set.h" #include "base/lazy_instance.h" #include "base/logging.h" @@ -15,7 +15,7 @@ namespace ui { namespace { -base::LazyInstance<std::unordered_set<int32_t>>::Leaky g_assigned_ids = +base::LazyInstance<base::flat_set<int32_t>>::Leaky g_assigned_ids = LAZY_INSTANCE_INITIALIZER; } // namespace diff --git a/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.h b/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.h index c42f3f4e77d..01fe3a72cbc 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.h +++ b/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.h @@ -7,16 +7,13 @@ #include "base/memory/raw_ptr.h" #include "ui/accessibility/ax_export.h" -#include "ui/accessibility/platform/inspect/ax_optional.h" #include "ui/accessibility/platform/inspect/ax_tree_indexer_mac.h" namespace ui { +class AXElementWrapper; class AXPropertyNode; -// Optional tri-state id object. -using AXOptionalNSObject = AXOptional<id>; - // Invokes a script instruction describing a call unit which represents // a sequence of calls. class AX_EXPORT AXCallStatementInvoker final { @@ -46,7 +43,7 @@ class AX_EXPORT AXCallStatementInvoker final { // Invokes a property node for a given AXElement. AXOptionalNSObject InvokeForAXElement( - const id target, + const AXElementWrapper& ax_element, const AXPropertyNode& property_node) const; // Invokes a property node for a given AXTextMarkerRange. @@ -65,7 +62,7 @@ class AX_EXPORT AXCallStatementInvoker final { // Invokes setAccessibilityFocused method. AXOptionalNSObject InvokeSetAccessibilityFocused( - const id target, + const AXElementWrapper& ax_element, const AXPropertyNode& property_node) const; // Returns a parameterized attribute parameter by a property node representing diff --git a/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.mm b/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.mm index 060fad61719..41663d9127c 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.mm +++ b/chromium/ui/accessibility/platform/inspect/ax_call_statement_invoker_mac.mm @@ -6,6 +6,8 @@ #include "base/strings/sys_string_conversions.h" #include "ui/accessibility/platform/ax_utils_mac.h" +#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" +#include "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h" #include "ui/accessibility/platform/inspect/ax_property_node.h" namespace ui { @@ -152,8 +154,8 @@ AXOptionalNSObject AXCallStatementInvoker::Invoke( AXOptionalNSObject AXCallStatementInvoker::InvokeFor( const id target, const AXPropertyNode& property_node) const { - if (IsNSAccessibilityElement(target) || IsAXUIElement(target)) - return InvokeForAXElement(target, property_node); + if (AXElementWrapper::IsValidElement(target)) + return InvokeForAXElement({target}, property_node); if (IsAXTextMarkerRange(target)) { return InvokeForAXTextMarkerRange(target, property_node); @@ -170,47 +172,48 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeFor( } AXOptionalNSObject AXCallStatementInvoker::InvokeForAXElement( - const id target, + const AXElementWrapper& ax_element, const AXPropertyNode& property_node) const { // Actions. if (property_node.name_or_value == "AXActionNames") { - return AXOptionalNSObject::NotNullOrNotApplicable(AXActionNamesOf(target)); + return AXOptionalNSObject::NotNullOrNotApplicable(ax_element.ActionNames()); } if (property_node.name_or_value == "AXPerformAction") { AXOptionalNSObject param = ParamFrom(property_node); if (param.IsNotNull()) { - PerformAXAction(target, *param); + ax_element.PerformAction(*param); return AXOptionalNSObject::Unsupported(); } return AXOptionalNSObject::Error(); } // Get or set attribute value if the attribute is supported. - for (NSString* attribute : AXAttributeNamesOf(target)) { + for (NSString* attribute : ax_element.AttributeNames()) { if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) { // Setter if (property_node.rvalue) { AXOptionalNSObject rvalue = Invoke(*property_node.rvalue); if (rvalue.IsNotNull()) { - SetAXAttributeValueOf(target, attribute, *rvalue); + ax_element.SetAttributeValue(attribute, *rvalue); return {rvalue}; } return rvalue; } // Getter. Make sure to expose null values in ax scripts. - id value = AXAttributeValueOf(target, attribute); - return IsDumpingTree() ? AXOptionalNSObject::NotNullOrNotApplicable(value) - : AXOptionalNSObject(value); + AXOptionalNSObject optional_value = + ax_element.GetAttributeValue(attribute); + return IsDumpingTree() + ? AXOptionalNSObject::NotNullOrNotApplicable(*optional_value) + : optional_value; } } // Parameterized attributes. - for (NSString* attribute : AXParameterizedAttributeNamesOf(target)) { + for (NSString* attribute : ax_element.ParameterizedAttributeNames()) { if (property_node.IsMatching(base::SysNSStringToUTF8(attribute))) { AXOptionalNSObject param = ParamFrom(property_node); if (param.IsNotNull()) { - return AXOptionalNSObject( - AXParameterizedAttributeValueOf(target, attribute, *param)); + return ax_element.GetParameterizedAttributeValue(attribute, *param); } return param; } @@ -240,33 +243,23 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeForAXElement( SEL selector = NSSelectorFromString(base::SysUTF8ToNSString(selector_string)); - if (![target respondsToSelector:selector]) + if (!ax_element.RespondsToSelector(selector)) return AXOptionalNSObject::Error(); - NSInvocation* invocation = [NSInvocation - invocationWithMethodSignature: - [[target class] instanceMethodSignatureForSelector:selector]]; - [invocation setSelector:selector]; - [invocation setTarget:target]; - if (optional_arg_selector) { - // The target is at index 0 and the selector at index 1, so arguments - // start at index 2. - [invocation setArgument:&*optional_arg_selector atIndex:2]; - } - [invocation invoke]; - BOOL return_value; - [invocation getReturnValue:&return_value]; + BOOL return_value = + optional_arg_selector + ? ax_element.Invoke<BOOL, SEL>(selector, *optional_arg_selector) + : ax_element.Invoke<BOOL>(selector); return AXOptionalNSObject([NSNumber numberWithBool:return_value]); } if (property_node.name_or_value == "setAccessibilityFocused") - return InvokeSetAccessibilityFocused(target, property_node); + return InvokeSetAccessibilityFocused(ax_element, property_node); // accessibilityAttributeValue if (property_node.name_or_value == "accessibilityAttributeValue") { if (property_node.arguments.size() == 1) { - return AXOptionalNSObject(AXAttributeValueOf( - target, + return AXOptionalNSObject(ax_element.GetAttributeValue( base::SysUTF8ToNSString(property_node.arguments[0].name_or_value))); } // Parameterized accessibilityAttributeValue. @@ -277,8 +270,8 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeForAXElement( if (!param.HasValue()) return AXOptionalNSObject::Error(); - return AXOptionalNSObject(AXParameterizedAttributeValueOf( - target, base::SysUTF8ToNSString(attribute), *param)); + return AXOptionalNSObject(ax_element.GetParameterizedAttributeValue( + base::SysUTF8ToNSString(attribute), *param)); } return AXOptionalNSObject::Error(); } @@ -286,14 +279,15 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeForAXElement( if (base::StartsWith(property_node.name_or_value, "accessibility")) { if (property_node.arguments.size() == 1) { absl::optional<id> optional_id = - PerformAXSelector(target, property_node.name_or_value, - property_node.arguments[0].name_or_value); + ax_element.PerformSelector(property_node.name_or_value, + property_node.arguments[0].name_or_value); if (optional_id) { return AXOptionalNSObject(*optional_id); } } if (property_node.arguments.empty()) { - auto optional_id = PerformAXSelector(target, property_node.name_or_value); + auto optional_id = + ax_element.PerformSelector(property_node.name_or_value); if (optional_id) { return AXOptionalNSObject(*optional_id); } @@ -399,7 +393,7 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeForDictionary( } AXOptionalNSObject AXCallStatementInvoker::InvokeSetAccessibilityFocused( - const id target, + const AXElementWrapper& ax_element, const AXPropertyNode& property_node) const { std::string selector_string = property_node.name_or_value + ":"; if (property_node.arguments.size() != 1) { @@ -410,24 +404,13 @@ AXOptionalNSObject AXCallStatementInvoker::InvokeSetAccessibilityFocused( } SEL selector = NSSelectorFromString(base::SysUTF8ToNSString(selector_string)); - if (![target respondsToSelector:selector]) { + if (!ax_element.RespondsToSelector(selector)) { LOG(ERROR) << "Target doesn't answer to " << selector_string << " selector"; return AXOptionalNSObject::Error(); } - NSInvocation* invocation = [NSInvocation - invocationWithMethodSignature: - [[target class] instanceMethodSignatureForSelector:selector]]; - - [invocation setSelector:selector]; - [invocation setTarget:target]; - - // The target is at index 0 and the selector at index 1, so arguments - // start at index 2. BOOL val = property_node.arguments[0].name_or_value == "FALSE" ? FALSE : TRUE; - [invocation setArgument:&val atIndex:2]; - [invocation invoke]; - + ax_element.Invoke<void, BOOL>(selector, val); return AXOptionalNSObject(nil); } diff --git a/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.h b/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.h new file mode 100644 index 00000000000..e02150cb618 --- /dev/null +++ b/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.h @@ -0,0 +1,158 @@ +// Copyright 2022 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. + +#ifndef UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_ELEMENT_WRAPPER_MAC_H_ +#define UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_ELEMENT_WRAPPER_MAC_H_ + +#import <Cocoa/Cocoa.h> + +#include "base/callback_forward.h" +#include "third_party/abseil-cpp/absl/types/optional.h" +#include "ui/accessibility/ax_export.h" +#include "ui/accessibility/platform/inspect/ax_inspect.h" +#include "ui/accessibility/platform/inspect/ax_optional.h" + +namespace ui { + +// Optional tri-state id object. +using AXOptionalNSObject = AXOptional<id>; + +// A wrapper around AXUIElement or NSAccessibilityElement object. +class AX_EXPORT AXElementWrapper final { + public: + // Returns true if the object is either NSAccessibilityElement or + // AXUIElement. + static bool IsValidElement(const id node); + + // Return true if the object is internal BrowserAccessibilityCocoa. + static bool IsNSAccessibilityElement(const id node); + + // Returns true if the object is AXUIElement. + static bool IsAXUIElement(const id node); + + // Returns the children of an accessible object, either AXUIElement or + // BrowserAccessibilityCocoa. + static NSArray* ChildrenOf(const id node); + + // Returns the DOM id of a given node (either AXUIElement or + // BrowserAccessibilityCocoa). + static std::string DOMIdOf(const id node); + + AXElementWrapper(const id node) : node_(node){}; + + // Returns true if the object is either an NSAccessibilityElement or + // AXUIElement. + bool IsValidElement() const; + + // Return true if the object is an internal BrowserAccessibilityCocoa. + bool IsNSAccessibilityElement() const; + + // Returns true if the object is an AXUIElement. + bool IsAXUIElement() const; + + // Returns the wrapped object. + id AsId() const; + + // Returns the DOM id of the object. + std::string DOMId() const; + + // Returns the children of the object. + NSArray* Children() const; + + // Returns the AXSize and AXPosition attributes for the object. + NSSize Size() const; + NSPoint Position() const; + + // Returns the (parameterized) attributes of the object. + NSArray* AttributeNames() const; + NSArray* ParameterizedAttributeNames() const; + + // Returns (parameterized) attribute value on the object. + AXOptionalNSObject GetAttributeValue(NSString* attribute) const; + AXOptionalNSObject GetParameterizedAttributeValue(NSString* attribute, + id parameter) const; + + // Performs the given selector on the object and returns the result. If + // the object does not conform to the NSAccessibility protocol or the selector + // is not found, then returns nullopt. + absl::optional<id> PerformSelector(const std::string& selector) const; + + // Performs the given selector on the object with exactly one string + // argument and returns the result. If the object does not conform to the + // NSAccessibility protocol or the selector is not found, then returns + // nullopt. + absl::optional<id> PerformSelector(const std::string& selector_string, + const std::string& argument_string) const; + + // Sets attribute value on the object. + void SetAttributeValue(NSString* attribute, id value) const; + + // Returns the list of actions supported on the object. + NSArray* ActionNames() const; + + // Performs the given action on the object. + void PerformAction(NSString* action) const; + + // Returns true if the object responds to the given selector. + bool RespondsToSelector(SEL selector) const { + return [node_ respondsToSelector:selector]; + } + + // Invokes a method of the given signature. + template <typename ReturnType, typename... Args> + typename std::enable_if<!std::is_same<ReturnType, void>::value, + ReturnType>::type + Invoke(SEL selector, Args... args) const { + NSInvocation* invocation = InvokeInternal(selector, args...); + ReturnType return_value; + [invocation getReturnValue:&return_value]; + return return_value; + } + + template <typename ReturnType, typename... Args> + typename std::enable_if<std::is_same<ReturnType, void>::value, void>::type + Invoke(SEL selector, Args... args) const { + InvokeInternal(selector, args...); + } + + private: + template <typename... Args> + NSInvocation* InvokeInternal(SEL selector, Args... args) const { + NSInvocation* invocation = [NSInvocation + invocationWithMethodSignature: + [[node_ class] instanceMethodSignatureForSelector:selector]]; + [invocation setSelector:selector]; + [invocation setTarget:node_]; + SetInvocationArguments<Args...>(invocation, 2, args...); + [invocation invoke]; + return invocation; + } + + template <typename Arg, typename... Args> + void SetInvocationArguments(NSInvocation* invocation, + int argument_index, + Arg& arg, + Args&... args) const { + [invocation setArgument:&arg atIndex:argument_index]; + SetInvocationArguments<Args...>(invocation, argument_index + 1, args...); + } + + template <typename... Args> + void SetInvocationArguments(NSInvocation*, int) const {} + + // Generates an error message from the given error. + std::string AXErrorMessage(AXError, const std::string& message) const; + + // Returns true on success, otherwise returns false and logs error. + bool AXSuccess(AXError result, const std::string& message) const; + + // Converts the given value and the error object into AXOptional object. + AXOptionalNSObject ToOptional(id, AXError, const std::string& message) const; + + const id node_; +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_ELEMENT_WRAPPER_MAC_H_ diff --git a/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.mm b/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.mm new file mode 100644 index 00000000000..a8bb7e90298 --- /dev/null +++ b/chromium/ui/accessibility/platform/inspect/ax_element_wrapper_mac.mm @@ -0,0 +1,362 @@ +// Copyright 2022 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 "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" + +#include <ostream> + +#include "base/callback.h" +#include "base/containers/fixed_flat_set.h" +#include "base/debug/stack_trace.h" +#include "base/logging.h" +#include "base/strings/pattern.h" +#include "base/strings/sys_string_conversions.h" +#include "ui/accessibility/platform/ax_private_attributes_mac.h" + +// error: 'accessibilityAttributeNames' is deprecated: first deprecated in +// macOS 10.10 - Use the NSAccessibility protocol methods instead (see +// NSAccessibilityProtocols.h +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +namespace ui { + +using base::SysNSStringToUTF8; + +constexpr char kUnsupportedObject[] = + "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + +// static +bool AXElementWrapper::IsValidElement(const id node) { + return AXElementWrapper(node).IsValidElement(); +} + +bool AXElementWrapper::IsNSAccessibilityElement(const id node) { + return AXElementWrapper(node).IsNSAccessibilityElement(); +} + +bool AXElementWrapper::IsAXUIElement(const id node) { + return AXElementWrapper(node).IsAXUIElement(); +} + +NSArray* AXElementWrapper::ChildrenOf(const id node) { + return AXElementWrapper(node).Children(); +} + +// Returns DOM id of a given node (either AXUIElement or +// BrowserAccessibilityCocoa). +std::string AXElementWrapper::DOMIdOf(const id node) { + return AXElementWrapper(node).DOMId(); +} + +bool AXElementWrapper::IsValidElement() const { + return IsNSAccessibilityElement() || IsAXUIElement(); +} + +bool AXElementWrapper::IsNSAccessibilityElement() const { + return [node_ isKindOfClass:[NSAccessibilityElement class]]; +} + +bool AXElementWrapper::IsAXUIElement() const { + return CFGetTypeID(node_) == AXUIElementGetTypeID(); +} + +id AXElementWrapper::AsId() const { + return node_; +} + +std::string AXElementWrapper::DOMId() const { + const id domid_value = + *GetAttributeValue(base::SysUTF8ToNSString("AXDOMIdentifier")); + return base::SysNSStringToUTF8(static_cast<NSString*>(domid_value)); +} + +NSArray* AXElementWrapper::Children() const { + if (IsNSAccessibilityElement()) + return [node_ children]; + + if (IsAXUIElement()) { + CFTypeRef children_ref; + if ((AXUIElementCopyAttributeValue(static_cast<AXUIElementRef>(node_), + kAXChildrenAttribute, &children_ref)) == + kAXErrorSuccess) + return static_cast<NSArray*>(children_ref); + return nil; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return nil; +} + +NSSize AXElementWrapper::Size() const { + if (IsNSAccessibilityElement()) { + return [node_ accessibilityFrame].size; + } + + if (!IsAXUIElement()) { + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return NSMakeSize(0, 0); + } + + id value = *GetAttributeValue(NSAccessibilitySizeAttribute); + if (value && CFGetTypeID(value) == AXValueGetTypeID()) { + AXValueType type = AXValueGetType(static_cast<AXValueRef>(value)); + if (type == kAXValueCGSizeType) { + NSSize size; + if (AXValueGetValue(static_cast<AXValueRef>(value), type, &size)) { + return size; + } + } + } + return NSMakeSize(0, 0); +} + +NSPoint AXElementWrapper::Position() const { + if (IsNSAccessibilityElement()) { + return [node_ accessibilityFrame].origin; + } + + if (!IsAXUIElement()) { + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return NSMakePoint(0, 0); + } + + id value = *GetAttributeValue(NSAccessibilityPositionAttribute); + if (value && CFGetTypeID(value) == AXValueGetTypeID()) { + AXValueType type = AXValueGetType(static_cast<AXValueRef>(value)); + if (type == kAXValueCGPointType) { + NSPoint point; + if (AXValueGetValue(static_cast<AXValueRef>(value), type, &point)) { + return point; + } + } + } + return NSMakePoint(0, 0); +} + +NSArray* AXElementWrapper::AttributeNames() const { + if (IsNSAccessibilityElement()) + return [node_ accessibilityAttributeNames]; + + if (IsAXUIElement()) { + CFArrayRef attributes_ref; + AXError result = AXUIElementCopyAttributeNames( + static_cast<AXUIElementRef>(node_), &attributes_ref); + if (AXSuccess(result, "AXAttributeNamesOf")) + return static_cast<NSArray*>(attributes_ref); + return nil; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return nil; +} + +NSArray* AXElementWrapper::ParameterizedAttributeNames() const { + if (IsNSAccessibilityElement()) + return [node_ accessibilityParameterizedAttributeNames]; + + if (IsAXUIElement()) { + CFArrayRef attributes_ref; + AXError result = AXUIElementCopyParameterizedAttributeNames( + static_cast<AXUIElementRef>(node_), &attributes_ref); + if (AXSuccess(result, "AXParameterizedAttributeNamesOf")) + return static_cast<NSArray*>(attributes_ref); + return nil; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return nil; +} + +AXOptionalNSObject AXElementWrapper::GetAttributeValue( + NSString* attribute) const { + if (IsNSAccessibilityElement()) + return AXOptionalNSObject([node_ accessibilityAttributeValue:attribute]); + + if (IsAXUIElement()) { + CFTypeRef value_ref; + AXError result = AXUIElementCopyAttributeValue( + static_cast<AXUIElementRef>(node_), static_cast<CFStringRef>(attribute), + &value_ref); + return ToOptional( + static_cast<id>(value_ref), result, + "AXGetAttributeValue(" + base::SysNSStringToUTF8(attribute) + ")"); + } + + return AXOptionalNSObject::Error(kUnsupportedObject); +} + +AXOptionalNSObject AXElementWrapper::GetParameterizedAttributeValue( + NSString* attribute, + id parameter) const { + if (IsNSAccessibilityElement()) + return AXOptionalNSObject([node_ accessibilityAttributeValue:attribute + forParameter:parameter]); + + if (IsAXUIElement()) { + // Convert NSValue parameter to CFTypeRef if needed. + CFTypeRef parameter_ref = static_cast<CFTypeRef>(parameter); + if ([parameter isKindOfClass:[NSValue class]] && + !strcmp([static_cast<NSValue*>(parameter) objCType], + @encode(NSRange))) { + NSRange range = [static_cast<NSValue*>(parameter) rangeValue]; + parameter_ref = AXValueCreate(kAXValueTypeCFRange, &range); + } + + // Get value. + CFTypeRef value_ref; + AXError result = AXUIElementCopyParameterizedAttributeValue( + static_cast<AXUIElementRef>(node_), static_cast<CFStringRef>(attribute), + parameter_ref, &value_ref); + + return ToOptional(static_cast<id>(value_ref), result, + "GetParameterizedAttributeValue(" + + base::SysNSStringToUTF8(attribute) + ")"); + } + + return AXOptionalNSObject::Error(kUnsupportedObject); +} + +absl::optional<id> AXElementWrapper::PerformSelector( + const std::string& selector_string) const { + if (![node_ conformsToProtocol:@protocol(NSAccessibility)]) + return absl::nullopt; + + NSString* selector_nsstring = base::SysUTF8ToNSString(selector_string); + SEL selector = NSSelectorFromString(selector_nsstring); + + if ([node_ respondsToSelector:selector]) + return [node_ valueForKey:selector_nsstring]; + return absl::nullopt; +} + +absl::optional<id> AXElementWrapper::PerformSelector( + const std::string& selector_string, + const std::string& argument_string) const { + if (![node_ conformsToProtocol:@protocol(NSAccessibility)]) + return absl::nullopt; + + SEL selector = + NSSelectorFromString(base::SysUTF8ToNSString(selector_string + ":")); + NSString* argument = base::SysUTF8ToNSString(argument_string); + + if ([node_ respondsToSelector:selector]) + return [node_ performSelector:selector withObject:argument]; + return absl::nullopt; +} + +void AXElementWrapper::SetAttributeValue(NSString* attribute, id value) const { + if (IsNSAccessibilityElement()) { + [node_ accessibilitySetValue:value forAttribute:attribute]; + return; + } + + if (IsAXUIElement()) { + AXUIElementSetAttributeValue(static_cast<AXUIElementRef>(node_), + static_cast<CFStringRef>(attribute), + static_cast<CFTypeRef>(value)); + return; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; +} + +NSArray* AXElementWrapper::ActionNames() const { + if (IsNSAccessibilityElement()) + return [node_ accessibilityActionNames]; + + if (IsAXUIElement()) { + CFArrayRef attributes_ref; + if ((AXUIElementCopyActionNames(static_cast<AXUIElementRef>(node_), + &attributes_ref)) == kAXErrorSuccess) + return static_cast<NSArray*>(attributes_ref); + return nil; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return nil; +} + +void AXElementWrapper::PerformAction(NSString* action) const { + if (IsNSAccessibilityElement()) { + [node_ accessibilityPerformAction:action]; + return; + } + + if (IsAXUIElement()) { + AXUIElementPerformAction(static_cast<AXUIElementRef>(node_), + static_cast<CFStringRef>(action)); + return; + } + + NOTREACHED() + << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; +} + +std::string AXElementWrapper::AXErrorMessage(AXError result, + const std::string& message) const { + if (result == kAXErrorSuccess) { + return {}; + } + + std::string error; + switch (result) { + case kAXErrorAttributeUnsupported: + error = "attribute unsupported"; + break; + case kAXErrorParameterizedAttributeUnsupported: + error = "parameterized attribute unsupported"; + break; + case kAXErrorNoValue: + error = "no value"; + break; + case kAXErrorIllegalArgument: + error = "illegal argument"; + break; + case kAXErrorInvalidUIElement: + error = "invalid UIElement"; + break; + case kAXErrorCannotComplete: + error = "cannot complete"; + break; + case kAXErrorNotImplemented: + error = "not implemented"; + break; + default: + error = "unknown error"; + break; + } + return {message + ": " + error}; +} + +bool AXElementWrapper::AXSuccess(AXError result, + const std::string& message) const { + std::string message_text = AXErrorMessage(result, message); + if (message_text.empty()) + return true; + + LOG(WARNING) << message_text; + return false; +} + +AXOptionalNSObject AXElementWrapper::ToOptional( + id value, + AXError result, + const std::string& message) const { + if (result == kAXErrorSuccess) + return AXOptionalNSObject(value); + + return AXOptionalNSObject::Error(AXErrorMessage(result, message)); +} + +} // namespace ui + +#pragma clang diagnostic pop diff --git a/chromium/ui/accessibility/platform/inspect/ax_inspect_scenario.h b/chromium/ui/accessibility/platform/inspect/ax_inspect_scenario.h index 3f870ae4872..5be2b665b22 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_inspect_scenario.h +++ b/chromium/ui/accessibility/platform/inspect/ax_inspect_scenario.h @@ -10,6 +10,7 @@ #include "third_party/abseil-cpp/absl/types/optional.h" #include "ui/accessibility/ax_export.h" +#include "ui/accessibility/platform/inspect/ax_inspect.h" namespace base { class FilePath; @@ -17,8 +18,6 @@ class FilePath; namespace ui { -struct AXPropertyFilter; -struct AXNodeFilter; class AXScriptInstruction; // Describes the test execution flow, which is parsed from a sequence diff --git a/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.h b/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.h index e223ecc7e80..3d4fcae136d 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.h +++ b/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.h @@ -16,97 +16,22 @@ using ui::AXTreeSelector; namespace ui { -// // Returns true if the given accessibility attribute is valid, and could have // been exposed on certain accessibility objects. AX_EXPORT bool IsValidAXAttribute(const std::string& attribute); -// -// Return true if the given object is internal BrowserAccessibilityCocoa. -AX_EXPORT bool IsNSAccessibilityElement(const id node); - -// -// Returns true if the given object is AXUIElement. -AX_EXPORT bool IsAXUIElement(const id node); - -// -// Returns children of an accessible object, either AXUIElement or -// BrowserAccessibilityCocoa. -AX_EXPORT NSArray* AXChildrenOf(const id node); - -// -// Returns AXSize and AXPosition attributes for an accessible object. -AX_EXPORT NSSize AXSizeOf(const id node); -AX_EXPORT NSPoint AXPositionOf(const id node); - -// -// Returns (parameterized) attributes of an accessible object, (either -// AXUIElement or BrowserAccessibilityCocoa). -AX_EXPORT NSArray* AXAttributeNamesOf(const id node); -AX_EXPORT NSArray* AXParameterizedAttributeNamesOf(const id node); - -// -// Returns (parameterized) attribute value on a given node (either AXUIElement -// or BrowserAccessibilityCocoa). -AX_EXPORT id AXAttributeValueOf(const id node, NSString* attribute); -AX_EXPORT id AXParameterizedAttributeValueOf(const id node, - NSString* attribute, - id parameter); - -// -// Performs the given selector on the given node and returns the result. If -// the node does not conform to the NSAccessibility protocol or the selector is -// not found, then returns nullopt. -AX_EXPORT absl::optional<id> PerformAXSelector(const id node, - const std::string& selector); - -// -// Performs the given selector on the given node with exactly one string -// argument and returns the result. If the node does not conform to the -// NSAccessibility protocol or the selector is not found, then returns nullopt. -AX_EXPORT absl::optional<id> PerformAXSelector( - const id node, - const std::string& selector_string, - const std::string& argument_string); - -// -// Sets attribute value on a given node (either AXUIElement or -// BrowserAccessibilityCocoa). -AX_EXPORT void SetAXAttributeValueOf(const id node, - NSString* attribute, - id value); - -// Returns a list of actions supported on a given accessible node (either -// AXUIElement or BrowserAccessibilityCocoa). -AX_EXPORT NSArray* AXActionNamesOf(const id node); - -// Performs action on a given accessible node (either AXUIElement or -// BrowserAccessibilityCocoa). -AX_EXPORT void PerformAXAction(const id node, NSString* action); - -// -// Returns DOM id of a given node (either AXUIElement or -// BrowserAccessibilityCocoa). -AX_EXPORT std::string GetDOMId(const id node); - -// // Return AXElement in a tree by a given criteria. using AXFindCriteria = base::RepeatingCallback<bool(const AXUIElementRef)>; AX_EXPORT AXUIElementRef FindAXUIElement(const AXUIElementRef node, const AXFindCriteria& criteria); -// // Returns AXUIElement and its application process id by a given tree selector. AX_EXPORT std::pair<AXUIElementRef, int> FindAXUIElement(const AXTreeSelector&); -// // Returns AXUIElement for a window having title matching the given pattern. AX_EXPORT AXUIElementRef FindAXWindowChild(AXUIElementRef parent, const std::string& pattern); -// Returns true on success, otherwise returns false and logs error. -AX_EXPORT bool AXSuccess(AXError, const std::string& message); - } // namespace ui #endif // UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_INSPECT_UTILS_MAC_H_ diff --git a/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.mm b/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.mm index 2a5a5b0f0a4..456192fdf02 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.mm +++ b/chromium/ui/accessibility/platform/inspect/ax_inspect_utils_mac.mm @@ -8,10 +8,12 @@ #include "base/callback.h" #include "base/containers/fixed_flat_set.h" +#include "base/debug/stack_trace.h" #include "base/logging.h" #include "base/strings/pattern.h" #include "base/strings/sys_string_conversions.h" #include "ui/accessibility/platform/ax_private_attributes_mac.h" +#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" // error: 'accessibilityAttributeNames' is deprecated: first deprecated in // macOS 10.10 - Use the NSAccessibility protocol methods instead (see @@ -92,251 +94,12 @@ bool IsValidAXAttribute(const std::string& attribute) { return kValidAttributes.contains(base::SysUTF8ToNSString(attribute)); } -bool IsNSAccessibilityElement(const id node) { - return [node isKindOfClass:[NSAccessibilityElement class]]; -} - -bool IsAXUIElement(const id node) { - return CFGetTypeID(node) == AXUIElementGetTypeID(); -} - NSArray* AXChildrenOf(const id node) { - if (IsNSAccessibilityElement(node)) - return [node children]; - - if (IsAXUIElement(node)) { - CFTypeRef children_ref; - if ((AXUIElementCopyAttributeValue(static_cast<AXUIElementRef>(node), - kAXChildrenAttribute, &children_ref)) == - kAXErrorSuccess) - return static_cast<NSArray*>(children_ref); - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -NSSize AXSizeOf(const id node) { - if (IsNSAccessibilityElement(node)) { - return [node accessibilityFrame].size; - } - - if (!IsAXUIElement(node)) { - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return NSMakeSize(0, 0); - } - - id value = AXAttributeValueOf(node, NSAccessibilitySizeAttribute); - if (value && CFGetTypeID(value) == AXValueGetTypeID()) { - AXValueType type = AXValueGetType(static_cast<AXValueRef>(value)); - if (type == kAXValueCGSizeType) { - NSSize size; - if (AXValueGetValue(static_cast<AXValueRef>(value), type, &size)) { - return size; - } - } - } - return NSMakeSize(0, 0); -} - -NSPoint AXPositionOf(const id node) { - if (IsNSAccessibilityElement(node)) { - return [node accessibilityFrame].origin; - } - - if (!IsAXUIElement(node)) { - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return NSMakePoint(0, 0); - } - - id value = AXAttributeValueOf(node, NSAccessibilityPositionAttribute); - if (value && CFGetTypeID(value) == AXValueGetTypeID()) { - AXValueType type = AXValueGetType(static_cast<AXValueRef>(value)); - if (type == kAXValueCGPointType) { - NSPoint point; - if (AXValueGetValue(static_cast<AXValueRef>(value), type, &point)) { - return point; - } - } - } - return NSMakePoint(0, 0); -} - -NSArray* AXAttributeNamesOf(const id node) { - if (IsNSAccessibilityElement(node)) - return [node accessibilityAttributeNames]; - - if (IsAXUIElement(node)) { - CFArrayRef attributes_ref; - AXError result = AXUIElementCopyAttributeNames( - static_cast<AXUIElementRef>(node), &attributes_ref); - if (AXSuccess(result, "AXAttributeNamesOf")) - return static_cast<NSArray*>(attributes_ref); - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -NSArray* AXParameterizedAttributeNamesOf(const id node) { - if (IsNSAccessibilityElement(node)) - return [node accessibilityParameterizedAttributeNames]; - - if (IsAXUIElement(node)) { - CFArrayRef attributes_ref; - AXError result = AXUIElementCopyParameterizedAttributeNames( - static_cast<AXUIElementRef>(node), &attributes_ref); - if (AXSuccess(result, "AXParameterizedAttributeNamesOf")) - return static_cast<NSArray*>(attributes_ref); - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -id AXAttributeValueOf(const id node, NSString* attribute) { - if (IsNSAccessibilityElement(node)) - return [node accessibilityAttributeValue:attribute]; - - if (IsAXUIElement(node)) { - CFTypeRef value_ref; - AXError result = AXUIElementCopyAttributeValue( - static_cast<AXUIElementRef>(node), static_cast<CFStringRef>(attribute), - &value_ref); - if (AXSuccess(result, "AXAttributeValueOf(" + - base::SysNSStringToUTF8(attribute) + ")")) - return static_cast<id>(value_ref); - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -id AXParameterizedAttributeValueOf(const id node, - NSString* attribute, - id parameter) { - if (IsNSAccessibilityElement(node)) - return [node accessibilityAttributeValue:attribute forParameter:parameter]; - - if (IsAXUIElement(node)) { - // Convert NSValue parameter to CFTypeRef if needed. - CFTypeRef parameter_ref = static_cast<CFTypeRef>(parameter); - if ([parameter isKindOfClass:[NSValue class]] && - !strcmp([static_cast<NSValue*>(parameter) objCType], - @encode(NSRange))) { - NSRange range = [static_cast<NSValue*>(parameter) rangeValue]; - parameter_ref = AXValueCreate(kAXValueTypeCFRange, &range); - } - - // Get value. - CFTypeRef value_ref; - AXError result = AXUIElementCopyParameterizedAttributeValue( - static_cast<AXUIElementRef>(node), static_cast<CFStringRef>(attribute), - parameter_ref, &value_ref); - if (AXSuccess(result, "AXParameterizedAttributeValueOf(" + - base::SysNSStringToUTF8(attribute) + ")")) - return static_cast<id>(value_ref); - - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -absl::optional<id> PerformAXSelector(const id node, - const std::string& selector_string) { - if (![node conformsToProtocol:@protocol(NSAccessibility)]) - return absl::nullopt; - - NSString* selector_nsstring = base::SysUTF8ToNSString(selector_string); - SEL selector = NSSelectorFromString(selector_nsstring); - - if ([node respondsToSelector:selector]) - return [node valueForKey:selector_nsstring]; - return absl::nullopt; -} - -absl::optional<id> PerformAXSelector(const id node, - const std::string& selector_string, - const std::string& argument_string) { - if (![node conformsToProtocol:@protocol(NSAccessibility)]) - return absl::nullopt; - - SEL selector = - NSSelectorFromString(base::SysUTF8ToNSString(selector_string + ":")); - NSString* argument = base::SysUTF8ToNSString(argument_string); - - if ([node respondsToSelector:selector]) - return [node performSelector:selector withObject:argument]; - return absl::nullopt; -} - -void SetAXAttributeValueOf(const id node, NSString* attribute, id value) { - if (IsNSAccessibilityElement(node)) { - [node accessibilitySetValue:value forAttribute:attribute]; - return; - } - - if (IsAXUIElement(node)) { - AXUIElementSetAttributeValue(static_cast<AXUIElementRef>(node), - static_cast<CFStringRef>(attribute), - static_cast<CFTypeRef>(value)); - return; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; -} - -NSArray* AXActionNamesOf(const id node) { - if (IsNSAccessibilityElement(node)) - return [node accessibilityActionNames]; - - if (IsAXUIElement(node)) { - CFArrayRef attributes_ref; - if ((AXUIElementCopyActionNames(static_cast<AXUIElementRef>(node), - &attributes_ref)) == kAXErrorSuccess) - return static_cast<NSArray*>(attributes_ref); - return nil; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; - return nil; -} - -void PerformAXAction(const id node, NSString* action) { - if (IsNSAccessibilityElement(node)) { - [node accessibilityPerformAction:action]; - return; - } - - if (IsAXUIElement(node)) { - AXUIElementPerformAction(static_cast<AXUIElementRef>(node), - static_cast<CFStringRef>(action)); - return; - } - - NOTREACHED() - << "Only AXUIElementRef and BrowserAccessibilityCocoa are supported."; + return AXElementWrapper(node).Children(); } std::string GetDOMId(const id node) { - const id domid_value = - AXAttributeValueOf(node, base::SysUTF8ToNSString("AXDOMIdentifier")); - return base::SysNSStringToUTF8(static_cast<NSString*>(domid_value)); + return AXElementWrapper(node).DOMId(); } AXUIElementRef FindAXUIElement(const AXUIElementRef node, @@ -408,8 +171,9 @@ std::pair<AXUIElementRef, int> FindAXUIElement(const AXTreeSelector& selector) { node, base::BindRepeating([](const AXUIElementRef node) { // Only active tab in exposed in browsers, thus find first // AXWebArea role. - NSString* role = AXAttributeValueOf(static_cast<id>(node), - NSAccessibilityRoleAttribute); + AXElementWrapper ax_node(static_cast<id>(node)); + NSString* role = + *ax_node.GetAttributeValue(NSAccessibilityRoleAttribute); return SysNSStringToUTF8(role) == "AXWebArea"; })); } @@ -428,54 +192,20 @@ AXUIElementRef FindAXWindowChild(AXUIElementRef parent, return nil; id window = [children objectAtIndex:0]; - NSString* role = AXAttributeValueOf(window, NSAccessibilityRoleAttribute); + + AXElementWrapper ax_window(window); + NSString* role = *ax_window.GetAttributeValue(NSAccessibilityRoleAttribute); if (SysNSStringToUTF8(role) != "AXWindow") return nil; NSString* window_title = - AXAttributeValueOf(window, NSAccessibilityTitleAttribute); + *ax_window.GetAttributeValue(NSAccessibilityTitleAttribute); if (base::MatchPattern(SysNSStringToUTF8(window_title), pattern)) return static_cast<AXUIElementRef>(window); return nil; } -AX_EXPORT bool AXSuccess(AXError result, const std::string& message) { - if (result == kAXErrorSuccess) { - return true; - } - - std::string error; - switch (result) { - case kAXErrorAttributeUnsupported: - error = "attribute unsupported"; - break; - case kAXErrorParameterizedAttributeUnsupported: - error = "parameterized attribute unsupported"; - break; - case kAXErrorNoValue: - error = "no value"; - break; - case kAXErrorIllegalArgument: - error = "illegal argument"; - break; - case kAXErrorInvalidUIElement: - error = "invalid UIElement"; - break; - case kAXErrorCannotComplete: - error = "cannot complete"; - break; - case kAXErrorNotImplemented: - error = "not implemented"; - break; - default: - error = "unknown error"; - break; - } - LOG(WARNING) << message << ": " << error; - return false; -} - } // namespace ui #pragma clang diagnostic pop diff --git a/chromium/ui/accessibility/platform/inspect/ax_optional.h b/chromium/ui/accessibility/platform/inspect/ax_optional.h index da0c195c607..aeb622c9efa 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_optional.h +++ b/chromium/ui/accessibility/platform/inspect/ax_optional.h @@ -18,7 +18,12 @@ template <typename ValueType> class AX_EXPORT AXOptional final { public: static constexpr AXOptional Unsupported() { return AXOptional(kUnsupported); } - static constexpr AXOptional Error() { return AXOptional(kError); } + static constexpr AXOptional Error(const char* error_text = nullptr) { + return error_text ? AXOptional(kError, error_text) : AXOptional(kError); + } + static constexpr AXOptional Error(const std::string& error_text) { + return AXOptional(kError, error_text); + } static constexpr AXOptional NotApplicable() { return AXOptional(kNotApplicable); } @@ -31,11 +36,11 @@ class AX_EXPORT AXOptional final { } explicit constexpr AXOptional(ValueType value_) - : value_(value_), flag_(kValue) {} + : value_(value_), state_(kValue) {} - bool constexpr IsUnsupported() const { return flag_ == kUnsupported; } - bool constexpr IsNotApplicable() const { return flag_ == kNotApplicable; } - bool constexpr IsError() const { return flag_ == kError; } + bool constexpr IsUnsupported() const { return state_ == kUnsupported; } + bool constexpr IsNotApplicable() const { return state_ == kNotApplicable; } + bool constexpr IsError() const { return state_ == kError; } template <typename T = ValueType> bool constexpr IsNotNull( @@ -49,9 +54,12 @@ class AX_EXPORT AXOptional final { return true; } - bool constexpr HasValue() { return flag_ == kValue; } + bool constexpr HasValue() { return state_ == kValue; } constexpr const ValueType& operator*() const { return value_; } + bool HasStateText() const { return !state_text_.empty(); } + std::string StateText() const { return state_text_; } + std::string ToString() const { if (IsNotNull()) return "<value>"; @@ -85,12 +93,14 @@ class AX_EXPORT AXOptional final { kUnsupported, }; - explicit constexpr AXOptional(State flag_) : value_(nullptr), flag_(flag_) {} - explicit constexpr AXOptional(ValueType value_, State flag_) - : value_(value_), flag_(flag_) {} + explicit constexpr AXOptional(State state, const std::string& state_text = {}) + : value_(nullptr), state_(state), state_text_(state_text) {} + explicit constexpr AXOptional(ValueType value, State state) + : value_(value), state_(state) {} ValueType value_; - State flag_; + State state_; + std::string state_text_; }; } // namespace ui diff --git a/chromium/ui/accessibility/platform/inspect/ax_property_node.h b/chromium/ui/accessibility/platform/inspect/ax_property_node.h index 64d34d1b2fd..ade1520c6b9 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_property_node.h +++ b/chromium/ui/accessibility/platform/inspect/ax_property_node.h @@ -5,7 +5,9 @@ #ifndef UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_PROPERTY_NODE_H_ #define UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_PROPERTY_NODE_H_ +#include <memory> #include <string> +#include <utility> #include <vector> #include "third_party/abseil-cpp/absl/types/optional.h" diff --git a/chromium/ui/accessibility/platform/inspect/ax_transform_mac.mm b/chromium/ui/accessibility/platform/inspect/ax_transform_mac.mm index 7cb34e119cd..397f01d7249 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_transform_mac.mm +++ b/chromium/ui/accessibility/platform/inspect/ax_transform_mac.mm @@ -11,6 +11,7 @@ #include "ui/accessibility/platform/ax_platform_node_delegate.h" #include "ui/accessibility/platform/ax_platform_tree_manager.h" #include "ui/accessibility/platform/ax_utils_mac.h" +#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" #include "ui/accessibility/platform/inspect/ax_inspect_utils.h" namespace ui { @@ -96,7 +97,7 @@ base::Value AXNSObjectToBaseValue(id value, const AXTreeIndexerMac* indexer) { return AXTextMarkerRangeToBaseValue(value, indexer); // Accessible object - if (IsNSAccessibilityElement(value) || IsAXUIElement(value)) { + if (AXElementWrapper::IsValidElement(value)) { return AXElementToBaseValue(value, indexer); } @@ -115,8 +116,8 @@ base::Value AXPositionToBaseValue( if (position->IsNullPosition()) return AXNilToBaseValue(); - const AXPlatformTreeManager* manager = static_cast<AXPlatformTreeManager*>( - AXTreeManagerMap::GetInstance().GetManager(position->tree_id())); + const AXPlatformTreeManager* manager = + static_cast<AXPlatformTreeManager*>(position->GetManager()); if (!manager) return AXNilToBaseValue(); diff --git a/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.h b/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.h index f011ed090c6..819f3ee5719 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.h +++ b/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.h @@ -46,12 +46,12 @@ class AX_EXPORT AXTreeFormatterMac : public AXTreeFormatterBase { base::Value BuildTree(const id root) const; base::Value BuildTreeForAXUIElement(AXUIElementRef node) const; - void RecursiveBuildTree(const id node, + void RecursiveBuildTree(const AXElementWrapper& ax_element, const NSRect& root_rect, const AXTreeIndexerMac* indexer, base::Value* dict) const; - void AddProperties(const id node, + void AddProperties(const AXElementWrapper& ax_element, const NSRect& root_rect, const AXTreeIndexerMac* indexer, base::Value* dict) const; @@ -62,7 +62,7 @@ class AX_EXPORT AXTreeFormatterMac : public AXTreeFormatterBase { const AXPropertyNode& property_node, const AXTreeIndexerMac* indexer) const; - base::Value PopulateLocalPosition(const id node, + base::Value PopulateLocalPosition(const AXElementWrapper& ax_element, const NSRect& root_rect) const; std::string ProcessTreeForOutput( diff --git a/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.mm b/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.mm index c40fbd7a50d..b224dc70a34 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.mm +++ b/chromium/ui/accessibility/platform/inspect/ax_tree_formatter_mac.mm @@ -11,6 +11,7 @@ #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "ui/accessibility/platform/ax_platform_node_cocoa.h" +#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" #include "ui/accessibility/platform/inspect/ax_inspect_scenario.h" #include "ui/accessibility/platform/inspect/ax_inspect_utils.h" #include "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h" @@ -32,10 +33,11 @@ namespace ui { namespace { -const char kLocalPositionDictAttr[] = "LocalPosition"; +constexpr char kLocalPositionDictAttr[] = "LocalPosition"; -const char kFailedToParseError[] = "_const_ERROR:FAILED_TO_PARSE"; -const char kNotApplicable[] = "_const_n/a"; +constexpr char kFailedPrefix[] = "_const_ERROR:"; +constexpr char kFailedToParseError[] = "_const_ERROR:FAILED_TO_PARSE"; +constexpr char kNotApplicable[] = "_const_n/a"; } // namespace @@ -85,11 +87,12 @@ base::Value AXTreeFormatterMac::BuildTree(const id root) const { AXTreeIndexerMac indexer(root); base::Value dict(base::Value::Type::DICTIONARY); - NSPoint position = AXPositionOf(root); - NSSize size = AXSizeOf(root); + AXElementWrapper ax_element(root); + NSPoint position = ax_element.Position(); + NSSize size = ax_element.Size(); NSRect rect = NSMakeRect(position.x, position.y, size.width, size.height); - RecursiveBuildTree(root, rect, &indexer, &dict); + RecursiveBuildTree(ax_element, rect, &indexer, &dict); return dict; } @@ -142,7 +145,9 @@ std::string AXTreeFormatterMac::EvaluateScript( base::Value result; if (value.IsError()) { - result = base::Value(kFailedToParseError); + result = value.HasStateText() + ? base::Value(kFailedPrefix + value.StateText()) + : base::Value(kFailedToParseError); } else if (value.IsNotApplicable()) { result = base::Value(kNotApplicable); } else { @@ -172,61 +177,64 @@ base::Value AXTreeFormatterMac::BuildNode(const id node) const { AXTreeIndexerMac indexer(node); base::Value dict(base::Value::Type::DICTIONARY); - NSPoint position = AXPositionOf(node); - NSSize size = AXSizeOf(node); + AXElementWrapper ax_element(node); + NSPoint position = ax_element.Position(); + NSSize size = ax_element.Size(); NSRect rect = NSMakeRect(position.x, position.y, size.width, size.height); - AddProperties(node, rect, &indexer, &dict); + AddProperties(ax_element, rect, &indexer, &dict); return dict; } -void AXTreeFormatterMac::RecursiveBuildTree(const id node, +void AXTreeFormatterMac::RecursiveBuildTree(const AXElementWrapper& ax_element, const NSRect& root_rect, const AXTreeIndexerMac* indexer, base::Value* dict) const { - AXPlatformNodeDelegate* platform_node = - IsNSAccessibilityElement(node) ? [node nodeDelegate] : nullptr; + AXPlatformNodeDelegate* platform_node = ax_element.IsNSAccessibilityElement() + ? [ax_element.AsId() nodeDelegate] + : nullptr; if (platform_node && !ShouldDumpNode(*platform_node)) return; - AddProperties(node, root_rect, indexer, dict); + AddProperties(ax_element, root_rect, indexer, dict); if (platform_node && !ShouldDumpChildren(*platform_node)) return; - NSArray* children = AXChildrenOf(node); + NSArray* children = ax_element.Children(); base::Value child_dict_list(base::Value::Type::LIST); for (id child in children) { base::Value child_dict(base::Value::Type::DICTIONARY); - RecursiveBuildTree(child, root_rect, indexer, &child_dict); + RecursiveBuildTree({child}, root_rect, indexer, &child_dict); child_dict_list.Append(std::move(child_dict)); } dict->SetPath(kChildrenDictAttr, std::move(child_dict_list)); } -void AXTreeFormatterMac::AddProperties(const id node, +void AXTreeFormatterMac::AddProperties(const AXElementWrapper& ax_element, const NSRect& root_rect, const AXTreeIndexerMac* indexer, base::Value* dict) const { // Chromium special attributes. - dict->SetPath(kLocalPositionDictAttr, PopulateLocalPosition(node, root_rect)); + dict->SetPath(kLocalPositionDictAttr, + PopulateLocalPosition(ax_element, root_rect)); // Dump all attributes if match-all filter is specified. if (HasMatchAllPropertyFilter()) { - NSArray* attributes = AXAttributeNamesOf(node); + NSArray* attributes = ax_element.AttributeNames(); for (NSString* attribute : attributes) { - dict->SetPath( - SysNSStringToUTF8(attribute), - AXNSObjectToBaseValue(AXAttributeValueOf(node, attribute), indexer)); + dict->SetPath(SysNSStringToUTF8(attribute), + AXNSObjectToBaseValue( + *ax_element.GetAttributeValue(attribute), indexer)); } return; } // Otherwise dump attributes matching allow filters only. - std::string line_index = indexer->IndexBy(node); + std::string line_index = indexer->IndexBy(ax_element.AsId()); for (const AXPropertyNode& property_node : PropertyFilterNodesFor(line_index)) { - AXCallStatementInvoker invoker(node, indexer); + AXCallStatementInvoker invoker(ax_element.AsId(), indexer); AXOptionalNSObject value = invoker.Invoke(property_node); if (value.IsNotApplicable() || value.IsUnsupported()) { continue; @@ -242,7 +250,7 @@ void AXTreeFormatterMac::AddProperties(const id node, } base::Value AXTreeFormatterMac::PopulateLocalPosition( - const id node, + const AXElementWrapper& ax_element, const NSRect& root_rect) const { // The NSAccessibility position of an object is in global coordinates and // based on the lower-left corner of the object. To make this easier and @@ -251,8 +259,8 @@ base::Value AXTreeFormatterMac::PopulateLocalPosition( int root_top = -static_cast<int>(root_rect.origin.y + root_rect.size.height); int root_left = static_cast<int>(root_rect.origin.x); - NSPoint node_position = AXPositionOf(node); - NSSize node_size = AXSizeOf(node); + NSPoint node_position = ax_element.Position(); + NSSize node_size = ax_element.Size(); return AXNSPointToBaseValue(NSMakePoint( static_cast<int>(node_position.x - root_left), diff --git a/chromium/ui/accessibility/platform/inspect/ax_tree_indexer_mac.h b/chromium/ui/accessibility/platform/inspect/ax_tree_indexer_mac.h index 6f9e8eb0cac..bd5904a55e5 100644 --- a/chromium/ui/accessibility/platform/inspect/ax_tree_indexer_mac.h +++ b/chromium/ui/accessibility/platform/inspect/ax_tree_indexer_mac.h @@ -5,7 +5,7 @@ #ifndef UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_TREE_INDEXER_MAC_H_ #define UI_ACCESSIBILITY_PLATFORM_INSPECT_AX_TREE_INDEXER_MAC_H_ -#include "ui/accessibility/platform/inspect/ax_inspect_utils_mac.h" +#include "ui/accessibility/platform/inspect/ax_element_wrapper_mac.h" #include "ui/accessibility/platform/inspect/ax_tree_indexer.h" namespace ui { @@ -15,12 +15,12 @@ namespace ui { struct AXNodeComparator { constexpr bool operator()(const gfx::NativeViewAccessible& lhs, const gfx::NativeViewAccessible& rhs) const { - if (IsAXUIElement(lhs)) { - DCHECK(IsAXUIElement(rhs)); + if (AXElementWrapper::IsAXUIElement(lhs)) { + DCHECK(AXElementWrapper::IsAXUIElement(rhs)); return CFHash(lhs) < CFHash(rhs); } - DCHECK(IsNSAccessibilityElement(lhs)); - DCHECK(IsNSAccessibilityElement(rhs)); + DCHECK(AXElementWrapper::IsNSAccessibilityElement(lhs)); + DCHECK(AXElementWrapper::IsNSAccessibilityElement(rhs)); return lhs < rhs; } }; @@ -28,9 +28,9 @@ struct AXNodeComparator { // // NSAccessibility tree indexer. using AXTreeIndexerMac = AXTreeIndexer<const gfx::NativeViewAccessible, - GetDOMId, + AXElementWrapper::DOMIdOf, NSArray*, - AXChildrenOf, + AXElementWrapper::ChildrenOf, AXNodeComparator>; } // namespace ui diff --git a/chromium/ui/accessibility/platform/test_ax_tree_update.cc b/chromium/ui/accessibility/platform/test_ax_tree_update.cc new file mode 100644 index 00000000000..5a021738ea1 --- /dev/null +++ b/chromium/ui/accessibility/platform/test_ax_tree_update.cc @@ -0,0 +1,56 @@ +// Copyright 2022 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 "ui/accessibility/platform/test_ax_tree_update.h" + +namespace ui { + +TestAXTreeUpdateNode::TestAXTreeUpdateNode(const TestAXTreeUpdateNode&) = + default; + +TestAXTreeUpdateNode::TestAXTreeUpdateNode(TestAXTreeUpdateNode&&) = default; + +TestAXTreeUpdateNode::~TestAXTreeUpdateNode() = default; + +TestAXTreeUpdateNode::TestAXTreeUpdateNode( + ax::mojom::Role role, + const std::vector<TestAXTreeUpdateNode>& children) + : children(children) { + DCHECK_NE(role, ax::mojom::Role::kUnknown); + data.role = role; +} + +TestAXTreeUpdateNode::TestAXTreeUpdateNode( + ax::mojom::Role role, + ax::mojom::State state, + const std::vector<TestAXTreeUpdateNode>& children) + : children(children) { + DCHECK_NE(role, ax::mojom::Role::kUnknown); + DCHECK_NE(state, ax::mojom::State::kNone); + data.role = role; + data.AddState(state); +} + +TestAXTreeUpdateNode::TestAXTreeUpdateNode(const std::string& text) { + data.role = ax::mojom::Role::kStaticText; + data.SetName(text); +} + +TestAXTreeUpdate::TestAXTreeUpdate(const TestAXTreeUpdateNode& root) { + root_id = SetSubtree(root); +} + +AXNodeID TestAXTreeUpdate::SetSubtree(const TestAXTreeUpdateNode& node) { + size_t node_index = nodes.size(); + nodes.push_back(node.data); + nodes[node_index].id = node_index + 1; + std::vector<AXNodeID> child_ids; + for (const auto& child : node.children) { + child_ids.push_back(SetSubtree(child)); + } + nodes[node_index].child_ids = child_ids; + return nodes[node_index].id; +} + +} // namespace ui diff --git a/chromium/ui/accessibility/platform/test_ax_tree_update.h b/chromium/ui/accessibility/platform/test_ax_tree_update.h new file mode 100644 index 00000000000..41878fa0920 --- /dev/null +++ b/chromium/ui/accessibility/platform/test_ax_tree_update.h @@ -0,0 +1,54 @@ +// Copyright 2022 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. + +#ifndef UI_ACCESSIBILITY_PLATFORM_TEST_AX_TREE_UPDATE_H_ +#define UI_ACCESSIBILITY_PLATFORM_TEST_AX_TREE_UPDATE_H_ + +#include "ui/accessibility/ax_tree_update.h" + +namespace ui { + +// These utility classes, help construct an AXTreeUpdate together with all of +// the updated nodes more easily. Only for use in tests for constructing / +// updating simple accessibility trees. + +// Used to construct AXTreeUpdate node. +struct TestAXTreeUpdateNode { + TestAXTreeUpdateNode() = delete; + ~TestAXTreeUpdateNode(); + + TestAXTreeUpdateNode(const TestAXTreeUpdateNode&); + TestAXTreeUpdateNode(TestAXTreeUpdateNode&&); + + TestAXTreeUpdateNode(ax::mojom::Role role, + const std::vector<TestAXTreeUpdateNode>& children); + TestAXTreeUpdateNode(ax::mojom::Role role, + ax::mojom::State state, + const std::vector<TestAXTreeUpdateNode>& children); + TestAXTreeUpdateNode(const std::string& text); + + AXNodeData data; + std::vector<TestAXTreeUpdateNode> children; +}; + +// Used to construct an accessible tree from a hierarchical list of nodes +// {<node_properties>, {<node_children>}}. For example, +// {Role::kRootWebArea, {"text"}} will create the following tree: +// kRootWebArea +// ++kStaticText "text" +class TestAXTreeUpdate final : public AXTreeUpdate { + public: + TestAXTreeUpdate(const TestAXTreeUpdateNode& root); + + TestAXTreeUpdate(const TestAXTreeUpdate&) = delete; + TestAXTreeUpdate& operator=(const TestAXTreeUpdate&) = delete; + + private: + // Recursively creates the tree update structure. + AXNodeID SetSubtree(const TestAXTreeUpdateNode& node); +}; + +} // namespace ui + +#endif // UI_ACCESSIBILITY_PLATFORM_TEST_AX_TREE_UPDATE_H_ diff --git a/chromium/ui/accessibility/test_ax_tree_manager.cc b/chromium/ui/accessibility/test_ax_tree_manager.cc index 5ee1750a265..392ba9146ff 100644 --- a/chromium/ui/accessibility/test_ax_tree_manager.cc +++ b/chromium/ui/accessibility/test_ax_tree_manager.cc @@ -13,93 +13,66 @@ namespace ui { TestAXTreeManager::TestAXTreeManager() = default; TestAXTreeManager::TestAXTreeManager(std::unique_ptr<AXTree> tree) - : tree_(std::move(tree)) { - if (tree_) - AXTreeManagerMap::GetInstance().AddTreeManager(GetTreeID(), this); -} + : AXTreeManager(std::move(tree)) {} TestAXTreeManager::TestAXTreeManager(TestAXTreeManager&& manager) - : tree_(std::move(manager.tree_)) { - if (tree_) { - AXTreeManagerMap::GetInstance().RemoveTreeManager(GetTreeID()); - AXTreeManagerMap::GetInstance().AddTreeManager(GetTreeID(), this); + : AXTreeManager(std::move(manager.ax_tree_)) { + if (ax_tree_) { + GetMap().RemoveTreeManager(GetTreeID()); + GetMap().AddTreeManager(GetTreeID(), this); } } TestAXTreeManager& TestAXTreeManager::operator=(TestAXTreeManager&& manager) { if (this == &manager) return *this; - if (manager.tree_) - AXTreeManagerMap::GetInstance().RemoveTreeManager(manager.GetTreeID()); + if (manager.ax_tree_) + GetMap().RemoveTreeManager(manager.GetTreeID()); // std::move(nullptr) == nullptr, so no need to check if `manager.tree_` is // assigned. - SetTree(std::move(manager.tree_)); + SetTree(std::move(manager.ax_tree_)); return *this; } -TestAXTreeManager::~TestAXTreeManager() { - if (tree_) - AXTreeManagerMap::GetInstance().RemoveTreeManager(GetTreeID()); -} +TestAXTreeManager::~TestAXTreeManager() = default; void TestAXTreeManager::DestroyTree() { - if (!tree_) + if (!ax_tree_) return; - AXTreeManagerMap::GetInstance().RemoveTreeManager(GetTreeID()); - tree_.reset(); + GetMap().RemoveTreeManager(GetTreeID()); + ax_tree_.reset(); } AXTree* TestAXTreeManager::GetTree() const { - DCHECK(tree_) << "Did you forget to call SetTree?"; - return tree_.get(); + DCHECK(ax_tree_) << "Did you forget to call SetTree?"; + return ax_tree_.get(); } void TestAXTreeManager::SetTree(std::unique_ptr<AXTree> tree) { - if (tree_) - AXTreeManagerMap::GetInstance().RemoveTreeManager(GetTreeID()); + if (ax_tree_) + GetMap().RemoveTreeManager(GetTreeID()); - tree_ = std::move(tree); - if (tree_) - AXTreeManagerMap::GetInstance().AddTreeManager(GetTreeID(), this); + ax_tree_ = std::move(tree); + ax_tree_id_ = GetTreeID(); + if (ax_tree_) + GetMap().AddTreeManager(GetTreeID(), this); } AXNode* TestAXTreeManager::GetNodeFromTree(const AXTreeID tree_id, const AXNodeID node_id) const { - return (tree_ && GetTreeID() == tree_id) ? tree_->GetFromId(node_id) - : nullptr; + return (ax_tree_ && GetTreeID() == tree_id) ? ax_tree_->GetFromId(node_id) + : nullptr; } AXNode* TestAXTreeManager::GetNodeFromTree(const AXNodeID node_id) const { - return tree_ ? tree_->GetFromId(node_id) : nullptr; -} - -void TestAXTreeManager::AddObserver(AXTreeObserver* observer) { - if (tree_) - tree_->AddObserver(observer); -} - -void TestAXTreeManager::RemoveObserver(AXTreeObserver* observer) { - if (tree_) - tree_->RemoveObserver(observer); -} - -AXTreeID TestAXTreeManager::GetTreeID() const { - return tree_ ? tree_->data().tree_id : AXTreeIDUnknown(); -} - -AXTreeID TestAXTreeManager::GetParentTreeID() const { - return tree_ ? tree_->data().parent_tree_id : AXTreeIDUnknown(); -} - -AXNode* TestAXTreeManager::GetRootAsAXNode() const { - return tree_ ? tree_->root() : nullptr; + return ax_tree_ ? ax_tree_->GetFromId(node_id) : nullptr; } AXNode* TestAXTreeManager::GetParentNodeFromParentTreeAsAXNode() const { AXTreeID parent_tree_id = GetParentTreeID(); - TestAXTreeManager* parent_manager = static_cast<TestAXTreeManager*>( - AXTreeManagerMap::GetInstance().GetManager(parent_tree_id)); + TestAXTreeManager* parent_manager = + static_cast<TestAXTreeManager*>(AXTreeManager::FromID(parent_tree_id)); if (!parent_manager) return nullptr; @@ -116,8 +89,4 @@ AXNode* TestAXTreeManager::GetParentNodeFromParentTreeAsAXNode() const { return nullptr; } -std::string TestAXTreeManager::ToString() const { - return "<TestAXTreeManager>"; -} - } // namespace ui diff --git a/chromium/ui/accessibility/test_ax_tree_manager.h b/chromium/ui/accessibility/test_ax_tree_manager.h index dabc5650132..a3299a3ac3a 100644 --- a/chromium/ui/accessibility/test_ax_tree_manager.h +++ b/chromium/ui/accessibility/test_ax_tree_manager.h @@ -30,7 +30,7 @@ class AX_EXPORT TestAXTreeManager : public AXTreeManager { // Takes ownership of |tree|. explicit TestAXTreeManager(std::unique_ptr<AXTree> tree); - virtual ~TestAXTreeManager(); + ~TestAXTreeManager() override; TestAXTreeManager(const TestAXTreeManager& manager) = delete; TestAXTreeManager& operator=(const TestAXTreeManager& manager) = delete; @@ -47,16 +47,7 @@ class AX_EXPORT TestAXTreeManager : public AXTreeManager { AXNode* GetNodeFromTree(const AXTreeID tree_id, const AXNodeID node_id) const override; AXNode* GetNodeFromTree(const AXNodeID node_id) const override; - void AddObserver(AXTreeObserver* observer) override; - void RemoveObserver(AXTreeObserver* observer) override; - AXTreeID GetTreeID() const override; - AXTreeID GetParentTreeID() const override; - AXNode* GetRootAsAXNode() const override; AXNode* GetParentNodeFromParentTreeAsAXNode() const override; - std::string ToString() const override; - - private: - std::unique_ptr<AXTree> tree_; }; } // namespace ui diff --git a/chromium/ui/accessibility/test_ax_tree_update_json_reader.cc b/chromium/ui/accessibility/test_ax_tree_update_json_reader.cc new file mode 100644 index 00000000000..11bc70b65f2 --- /dev/null +++ b/chromium/ui/accessibility/test_ax_tree_update_json_reader.cc @@ -0,0 +1,342 @@ +// Copyright 2022 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 "ui/accessibility/test_ax_tree_update_json_reader.h" + +#include "base/containers/contains.h" +#include "base/containers/flat_set.h" +#include "base/numerics/clamped_math.h" +#include "base/strings/string_split.h" +#include "ui/accessibility/ax_enum_util.h" + +namespace { + +using RoleConversions = const std::map<std::string, ax::mojom::Role>; + +// The 3 lists below include all terms that are not parsed now if they are in a +// JSON file. Since this class is only used for testing, we will only encounter +// errors regarding that in the following two cases: +// - We add a new JSON file (or modify one) for testing that includes different +// unsupported. +// - There is a new property added to AXNode that is not covered in the existing +// JSON parsor and a test relies on it. +// In both above cases, existing tests will catch the issue and warn about the +// missing/changed property. +const base::flat_set<std::string> kUnusedAxNodeProperties = { + "controls", "describedby", "details", "disabled", "editable", + "focused", "hidden", "hiddenRoot", "live", "multiline", + "readonly", "relevant", "required", "settable"}; + +const base::flat_set<std::string> kUnusedAxNodeItems = { + "frameId", "ignoredReasons", "parentId"}; + +const base::flat_set<std::string> kUnusedStyles = { + "background-image", "background-size", "clip", "font-style", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "opacity", "padding-bottom", "padding-left", "padding-right", + "padding-top", "position", "text-align", "text-decoration", + "z-index"}; + +int GetAsInt(const base::Value& value) { + if (value.is_int()) + return value.GetInt(); + if (value.is_string()) + return atoi(value.GetString().c_str()); + + NOTREACHED() << "Unexpected: " << value; + return 0; +} + +double GetAsDouble(const base::Value& value) { + if (value.is_double()) + return value.GetDouble(); + if (value.is_int()) + return value.GetInt(); + if (value.is_string()) + return atof(value.GetString().c_str()); + + NOTREACHED() << "Unexpected: " << value; + return 0; +} + +bool GetAsBoolean(const base::Value& value) { + if (value.is_bool()) + return value.GetBool(); + if (value.is_string()) { + if (value.GetString() == "false") + return false; + if (value.GetString() == "true") + return true; + } + + NOTREACHED() << "Unexpected: " << value; + return false; +} + +void GetTypeAndValue(const base::Value& node, + std::string& type, + std::string& value) { + type = node.GetDict().Find("type")->GetString(); + value = node.GetDict().Find("value")->GetString(); +} + +ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update, + const base::Value& node, + RoleConversions* role_conversions); + +void ParseAxNodeChildIds(ui::AXNodeData& node_data, + const base::Value& child_ids) { + for (const auto& item : child_ids.GetList()) + node_data.child_ids.push_back(GetAsInt(item)); +} + +void ParseAxNodeDescription(ui::AXNodeData& node_data, + const base::Value& description) { + std::string type, value; + GetTypeAndValue(description, type, value); + DCHECK_EQ(type, "computedString"); + node_data.SetDescription(value); +} + +void ParseAxNodeName(ui::AXNodeData& node_data, const base::Value& name) { + std::string type, value; + GetTypeAndValue(name, type, value); + DCHECK_EQ(type, "computedString"); + node_data.SetName(value); +} + +void ParseAxNodeProperties(ui::AXNodeData& node_data, + const base::Value& properties) { + if (properties.is_list()) { + for (const auto& item : properties.GetList()) + ParseAxNodeProperties(node_data, item); + return; + } + + const std::string prop_type = properties.GetDict().Find("name")->GetString(); + const base::Value* prop_value = + properties.GetDict().Find("value")->GetDict().Find("value"); + + if (prop_type == "atomic") { + node_data.AddBoolAttribute( + ax::mojom::BoolAttribute::kNonAtomicTextFieldRoot, + !GetAsBoolean(*prop_value)); + } else if (prop_type == "focusable") { + if (GetAsBoolean(*prop_value)) + node_data.AddState(ax::mojom::State::kFocusable); + } else if (prop_type == "expanded") { + if (GetAsBoolean(*prop_value)) + node_data.AddState(ax::mojom::State::kExpanded); + } else if (prop_type == "hasPopup") { + node_data.SetHasPopup( + ui::ParseAXEnum<ax::mojom::HasPopup>(prop_value->GetString().c_str())); + } else if (prop_type == "invalid") { + node_data.SetInvalidState(GetAsBoolean(*prop_value) + ? ax::mojom::InvalidState::kTrue + : ax::mojom::InvalidState::kFalse); + } else if (prop_type == "level") { + node_data.AddIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel, + GetAsInt(*prop_value)); + } else { + DCHECK(base::Contains(kUnusedAxNodeProperties, prop_type)) << prop_type; + } +} + +ax::mojom::Role RoleFromString(std::string role, + RoleConversions* role_conversions) { + const auto& item = role_conversions->find(role); + DCHECK(item != role_conversions->end()) << role; + return item->second; +} + +void ParseAxNodeRole(ui::AXNodeData& node_data, + const base::Value& role, + RoleConversions* role_conversions) { + const std::string role_type = role.GetDict().Find("type")->GetString(); + std::string role_value = role.GetDict().Find("value")->GetString(); + + DCHECK(role_type == "role" || role_type == "internalRole"); + + node_data.role = RoleFromString(role_value, role_conversions); +} + +void ParseAxNode(ui::AXNodeData& node_data, + const base::Value& ax_node, + RoleConversions* role_conversions) { + // Store the name and set it at the end because |AXNodeData::SetName| + // expects a valid role to have already been set prior to calling it. + base::Value name_value; + for (const auto item : ax_node.GetDict()) { + if (item.first == "backendDOMNodeId") { + node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId, + GetAsInt(item.second)); + } else if (item.first == "childIds") { + ParseAxNodeChildIds(node_data, item.second); + } else if (item.first == "description") { + ParseAxNodeDescription(node_data, item.second); + } else if (item.first == "ignored") { + DCHECK(item.second.is_bool()); + if (item.second.GetBool()) + node_data.AddState(ax::mojom::State::kIgnored); + } else if (item.first == "name") { + name_value = item.second.Clone(); + } else if (item.first == "nodeId") { + node_data.id = GetAsInt(item.second); + } else if (item.first == "properties") { + ParseAxNodeProperties(node_data, item.second); + } else if (item.first == "role") { + ParseAxNodeRole(node_data, item.second, role_conversions); + } else { + DCHECK(base::Contains(kUnusedAxNodeItems, item.first)) << item.first; + } + } + if (!name_value.is_none()) + ParseAxNodeName(node_data, name_value); +} + +void ParseChildren(ui::AXTreeUpdate& tree_update, + const base::Value& children, + RoleConversions* role_conversions) { + for (const auto& child : children.GetList()) + AddNode(tree_update, child, role_conversions); +} + +// Converts "rgb(R,G,B)" or "rgba(R,G,B,A)" to one ARGB integer where R,G, and B +// are integers and A is float < 1. +uint32_t ConvertRgbaStringToArgbInt(const std::string& argb_string) { + std::vector<std::string> values = base::SplitString( + argb_string, ",()", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + + uint32_t a, r, g, b; + + if (values.size() == 4 && values[0] == "rgb") { + a = 0; + } else if (values.size() == 5 && values[0] == "rgba") { + a = base::ClampRound(atof(values[4].c_str()) * 255); + } else { + NOTREACHED() << "Unexpected color value: " << argb_string; + return -1; + } + + r = atoi(values[1].c_str()); + g = atoi(values[2].c_str()); + b = atoi(values[3].c_str()); + + return (a << 24) + (r << 16) + (g << 8) + b; +} + +void ParseStyle(ui::AXNodeData& node_data, const base::Value& style) { + const std::string& name = style.GetDict().Find("name")->GetString(); + const std::string& value = style.GetDict().Find("value")->GetString(); + + if (name == "color") { + node_data.AddIntAttribute(ax::mojom::IntAttribute::kColor, + ConvertRgbaStringToArgbInt(value)); + } else if (name == "direction") { + node_data.AddIntAttribute( + ax::mojom::IntAttribute::kTextDirection, + static_cast<int>( + ui::ParseAXEnum<ax::mojom::WritingDirection>(value.c_str()))); + } else if (name == "display") { + node_data.AddStringAttribute(ax::mojom::StringAttribute::kDisplay, value); + } else if (name == "font-size") { + // Drop the 'px' at the end of font size. + DCHECK(style.GetDict().Find("value")->is_string()); + node_data.AddFloatAttribute( + ax::mojom::FloatAttribute::kFontSize, + atof(value.substr(0, value.length() - 2).c_str())); + } else if (name == "font-weight") { + DCHECK(style.GetDict().Find("value")->is_string()); + node_data.AddFloatAttribute(ax::mojom::FloatAttribute::kFontSize, + atof(value.c_str())); + } else if (name == "list-style-type") { + node_data.AddIntAttribute( + ax::mojom::IntAttribute::kListStyle, + static_cast<int>(ui::ParseAXEnum<ax::mojom::ListStyle>(value.c_str()))); + } else if (name == "visibility") { + if (value == "hidden") + node_data.AddState(ax::mojom::State::kInvisible); + else + DCHECK_EQ(value, "visible"); + } else { + DCHECK(base::Contains(kUnusedStyles, name)) << name; + } +} + +void ParseExtras(ui::AXNodeData& node_data, const base::Value& extras) { + for (const auto extra : extras.GetDict()) { + const base::Value::List& items = extra.second.GetList(); + if (extra.first == "bounds") { + node_data.relative_bounds.bounds.set_x(GetAsDouble(items[0])); + node_data.relative_bounds.bounds.set_y(GetAsDouble(items[1])); + node_data.relative_bounds.bounds.set_width(GetAsDouble(items[2])); + node_data.relative_bounds.bounds.set_height(GetAsDouble(items[3])); + } else if (extra.first == "styles") { + for (const auto& style : items) + ParseStyle(node_data, style); + } else { + NOTREACHED() << "Unexpected: " << extra.first; + } + } +} + +// Adds a node and returns its id. +ui::AXNodeID AddNode(ui::AXTreeUpdate& tree_update, + const base::Value& node, + RoleConversions* role_conversions) { + ui::AXNodeData node_data; + + // Store the string and set it at the end because |AXNodeData::SetName| + // expects a valid role to have already been set prior to calling it. + std::string name_string; + + for (const auto item : node.GetDict()) { + if (item.first == "axNode") { + ParseAxNode(node_data, item.second, role_conversions); + } else if (item.first == "backendDomId") { + node_data.AddIntAttribute(ax::mojom::IntAttribute::kDOMNodeId, + GetAsInt(item.second)); + } else if (item.first == "children") { + ParseChildren(tree_update, item.second, role_conversions); + } else if (item.first == "description") { + node_data.SetDescription(item.second.GetString()); + } else if (item.first == "extras") { + ParseExtras(node_data, item.second); + } else if (item.first == "interesting") { + // Not used yet, boolean. + } else if (item.first == "name") { + name_string = item.second.GetString(); + } else if (item.first == "role") { + node_data.role = + RoleFromString(item.second.GetString(), role_conversions); + } else { + NOTREACHED() << "Unexpected: " << item.first; + } + } + + node_data.SetName(name_string); + + tree_update.nodes.push_back(node_data); + + return node_data.id; +} + +} // namespace + +namespace ui { + +AXTreeUpdate AXTreeUpdateFromJSON(const base::Value& json, + RoleConversions* role_conversions) { + AXTreeUpdate tree_update; + + // Input should be a list with one item, which is the root node. + DCHECK(json.is_list() && json.GetList().size() == 1); + + tree_update.root_id = + AddNode(tree_update, json.GetList().front(), role_conversions); + + return tree_update; +} + +} // namespace ui diff --git a/chromium/ui/accessibility/test_ax_tree_update_json_reader.h b/chromium/ui/accessibility/test_ax_tree_update_json_reader.h new file mode 100644 index 00000000000..2fd51c37115 --- /dev/null +++ b/chromium/ui/accessibility/test_ax_tree_update_json_reader.h @@ -0,0 +1,27 @@ +// Copyright 2022 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. + +#ifndef UI_ACCESSIBILITY_TEST_AX_TREE_UPDATE_JSON_READER_H_ +#define UI_ACCESSIBILITY_TEST_AX_TREE_UPDATE_JSON_READER_H_ + +#include "base/values.h" +#include "ui/accessibility/ax_tree.h" + +namespace ui { + +// This function assumes that the JSON input is properly formatted and any +// error in parsing can result in a runtime error. +// The JSON format is based on the output of +// |InspectorAccessibilityAgent::WalkAXNodesToDepth| and should stay in sync +// with that. +// NOTE: This parser is not complete and only processes the required tags for +// the existing tests. +// |role_conversions| is a map of role strings in the JSON file to Chrome roles. +// TODO(https://crbug.com/1278249): Drop |role_conversions| once Chrome roles +// are added to the JSON file. +AXTreeUpdate AXTreeUpdateFromJSON( + const base::Value& json, + const std::map<std::string, ax::mojom::Role>* role_conversions); +} // namespace ui +#endif // UI_ACCESSIBILITY_TEST_AX_TREE_UPDATE_JSON_READER_H_ |