910e62b5创建于 1月15日历史提交
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/ax_language_detection.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>

#include "base/command_line.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_tree.h"

namespace ui {

const std::string kTextEnglish =
    "This is text created using Google Translate, it is unlikely to be "
    "idiomatic in the given target language. This text is only used to "
    "test language detection";

const std::string kTextFrench =
    "Ce texte a été créé avec Google Translate, il est peu probable qu'il "
    "soit idiomatique dans la langue cible indiquée Ce texte est "
    "uniquement utilisé pour tester la détection de la langue.";

const std::string kTextGerman =
    "Dies ist ein mit Google Translate erstellter Text. Es ist "
    "unwahrscheinlich, dass er in der angegebenen Zielsprache idiomatisch "
    "ist. Dieser Text wird nur zum Testen der Spracherkennung verwendet.";

const std::string kTextSpanish =
    "Este es un texto creado usando Google Translate, es poco probable que sea "
    "idiomático en el idioma de destino dado. Este texto solo se usa para "
    "probar la detección de idioma.";

// This test fixture is a friend of classes in ax_language_detection.h in order
// to enable testing of internals.
//
// When used with TEST_F, the test body is a subclass of this fixture, so we
// need to re-expose any members through this fixture in order for them to
// be accessible from within the test body.
class AXLanguageDetectionTestFixture : public testing::Test {
 public:
  AXLanguageDetectionTestFixture() = default;
  ~AXLanguageDetectionTestFixture() override = default;

  AXLanguageDetectionTestFixture(const AXLanguageDetectionTestFixture&) =
      delete;
  AXLanguageDetectionTestFixture& operator=(
      const AXLanguageDetectionTestFixture&) = delete;

 protected:
  bool IsStaticLanguageDetectionEnabled() {
    return AXLanguageDetectionManager::IsStaticLanguageDetectionEnabled();
  }

  bool IsDynamicLanguageDetectionEnabled() {
    return AXLanguageDetectionManager::IsDynamicLanguageDetectionEnabled();
  }

  AXLanguageDetectionObserver* getObserver(AXTree& tree) {
    return tree.language_detection_manager->language_detection_observer_.get();
  }

  int get_score(AXTree& tree, const std::string& lang) {
    return tree.language_detection_manager->lang_info_stats_.GetScore(lang);
  }

  // Accessors for testing metric data.
  void disable_metric_clearing(AXTree& tree) {
    tree.language_detection_manager->lang_info_stats_.disable_metric_clearing_ =
        true;
  }

  int count_detection_attempted(AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_
        .count_detection_attempted_;
  }

  int count_detection_results(AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_
        .count_detection_results_;
  }

  int count_labelled(AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_.count_labelled_;
  }

  int count_labelled_with_top_result(AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_
        .count_labelled_with_top_result_;
  }

  int count_overridden(AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_.count_overridden_;
  }

  const absl::flat_hash_set<std::string>& unique_top_lang_detected(
      AXTree& tree) const {
    return tree.language_detection_manager->lang_info_stats_
        .unique_top_lang_detected_;
  }
};

class AXLanguageDetectionTestStaticContent
    : public AXLanguageDetectionTestFixture {
 public:
  AXLanguageDetectionTestStaticContent() = default;
  ~AXLanguageDetectionTestStaticContent() override = default;

  AXLanguageDetectionTestStaticContent(
      const AXLanguageDetectionTestStaticContent&) = delete;
  AXLanguageDetectionTestStaticContent& operator=(
      const AXLanguageDetectionTestStaticContent&) = delete;

  void SetUp() override {
    AXLanguageDetectionTestFixture::SetUp();

    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        ::switches::kEnableExperimentalAccessibilityLanguageDetection);
  }
};

class AXLanguageDetectionTestDynamicContent
    : public AXLanguageDetectionTestStaticContent {
 public:
  AXLanguageDetectionTestDynamicContent() = default;
  ~AXLanguageDetectionTestDynamicContent() override = default;

  AXLanguageDetectionTestDynamicContent(
      const AXLanguageDetectionTestDynamicContent&) = delete;
  AXLanguageDetectionTestDynamicContent& operator=(
      const AXLanguageDetectionTestDynamicContent&) = delete;

  void SetUp() override {
    AXLanguageDetectionTestStaticContent::SetUp();

    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);
  }
};

TEST_F(AXLanguageDetectionTestFixture, StaticContentFeatureFlag) {
  // TODO(crbug.com/41417304): Remove this test once this feature is stable
  EXPECT_FALSE(
      ::switches::IsExperimentalAccessibilityLanguageDetectionEnabled());
  EXPECT_FALSE(IsStaticLanguageDetectionEnabled());

  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);

  EXPECT_TRUE(
      ::switches::IsExperimentalAccessibilityLanguageDetectionEnabled());
  EXPECT_TRUE(IsStaticLanguageDetectionEnabled());
}

TEST_F(AXLanguageDetectionTestFixture, DynamicContentFeatureFlag) {
  // TODO(crbug.com/41417304): Remove this test once this feature is stable
  EXPECT_FALSE(
      ::switches::IsExperimentalAccessibilityLanguageDetectionDynamicEnabled());
  EXPECT_FALSE(IsDynamicLanguageDetectionEnabled());

  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);

  EXPECT_TRUE(
      ::switches::IsExperimentalAccessibilityLanguageDetectionDynamicEnabled());
  EXPECT_TRUE(IsDynamicLanguageDetectionEnabled());
}

TEST_F(AXLanguageDetectionTestFixture, FeatureFlag) {
  // TODO(crbug.com/41417304): Remove this test once this feature is stable
  EXPECT_FALSE(IsStaticLanguageDetectionEnabled());
  EXPECT_FALSE(IsDynamicLanguageDetectionEnabled());

  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {features::kEnableAccessibilityLanguageDetection}, {});

  EXPECT_TRUE(IsStaticLanguageDetectionEnabled());
  EXPECT_TRUE(IsDynamicLanguageDetectionEnabled());
}

TEST(AXLanguageDetectionTest, LangAttrInheritanceFeatureFlagOff) {
  // Test lang attribute inheritance when feature flag is off.
  //
  // Lang attribute inheritance is handled by GetLanguage.
  //
  // Tree:
  //        1
  //      2   3
  //    4
  //  5
  //
  //  1 - English lang attribute
  //  2 - French lang attribute
  //
  //  Expected:
  //    3 - inherit English from 1
  //    4 - inherit French from 2
  //    5 - inherit Frnech from 4/2
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(5);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kGenericContainer;
    node2.child_ids.resize(1);
    node2.child_ids[0] = 4;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
  }

  {
    AXNodeData& node4 = initial_state.nodes[3];
    node4.id = 4;
    node4.role = ax::mojom::Role::kStaticText;
    node4.child_ids.resize(1);
    node4.child_ids[0] = 5;
  }

  {
    AXNodeData& node5 = initial_state.nodes[4];
    node5.id = 5;
    node5.role = ax::mojom::Role::kInlineTextBox;
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  {
    AXNode* node1 = tree.GetFromId(1);
    EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node1->GetLanguage(), "en");
  }

  {
    AXNode* node2 = tree.GetFromId(2);
    EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node2->GetLanguage(), "fr");
  }

  {
    AXNode* node3 = tree.GetFromId(3);
    EXPECT_EQ(node3->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node3->GetLanguage(), "en");
  }

  {
    AXNode* node4 = tree.GetFromId(4);
    EXPECT_EQ(node4->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node4->GetLanguage(), "fr");
  }

  {
    AXNode* node5 = tree.GetFromId(5);
    EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node5->GetLanguage(), "fr");
  }
}

TEST(AXLanguageDetectionTest, LangAttrInheritanceFeatureFlagOn) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);

  // Test lang attribute inheritance in the absence of any detected language.
  //
  // Lang attribute inheritance is handled by the Label step.
  //
  // Tree:
  //        1
  //      2   3
  //    4
  //  5
  //
  //  1 - English lang attribute
  //  2 - French lang attribute
  //
  //  Expected:
  //    3 - inherit English from 1
  //    4 - inherit French from 2
  //    5 - inherit Frnech from 4/2

  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(5);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kGenericContainer;
    node2.child_ids.resize(1);
    node2.child_ids[0] = 4;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
  }

  {
    AXNodeData& node4 = initial_state.nodes[3];
    node4.id = 4;
    node4.role = ax::mojom::Role::kStaticText;
    node4.child_ids.resize(1);
    node4.child_ids[0] = 5;
  }

  {
    AXNodeData& node5 = initial_state.nodes[4];
    node5.id = 5;
    node5.role = ax::mojom::Role::kInlineTextBox;
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  {
    AXNode* node1 = tree.GetFromId(1);
    // No detection for non text nodes.
    EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node1->GetLanguage(), "en");
  }

  {
    AXNode* node2 = tree.GetFromId(2);
    EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node2->GetLanguage(), "fr");
  }

  {
    AXNode* node3 = tree.GetFromId(3);
    // Inherited languages are not stored in lang info.
    EXPECT_EQ(node3->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node3->GetLanguage(), "en");
  }

  {
    AXNode* node4 = tree.GetFromId(4);
    // Inherited languages are not stored in lang info.
    EXPECT_EQ(node4->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node4->GetLanguage(), "fr");
  }

  {
    AXNode* node5 = tree.GetFromId(5);
    // Inherited languages are not stored in lang info.
    EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node5->GetLanguage(), "fr");
  }
}

// Tests that AXNode::GetLanguage() terminates when there is no lang attribute.
TEST_F(AXLanguageDetectionTestStaticContent, GetLanguageBoringTree) {
  // This test checks the behaviour of Detect, Label, and GetLanguage on a
  // 'boring' tree.
  //
  // The tree built here contains no lang attributes, nor does it contain any
  // text to perform detection on.
  //
  // Tree:
  //      1
  //    2   3
  //  4
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(4);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids.resize(2);
  initial_state.nodes[0].child_ids[0] = 2;
  initial_state.nodes[0].child_ids[1] = 3;
  initial_state.nodes[1].id = 2;
  initial_state.nodes[1].child_ids.resize(1);
  initial_state.nodes[1].child_ids[0] = 4;
  initial_state.nodes[2].id = 3;
  initial_state.nodes[3].id = 4;

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Check that tree parenting conforms to expected shape.
  AXNode* node1 = tree.GetFromId(1);
  EXPECT_EQ(node1->parent(), nullptr);

  AXNode* node2 = tree.GetFromId(2);
  ASSERT_EQ(node2->parent(), node1);
  EXPECT_EQ(node2->parent()->parent(), nullptr);

  AXNode* node3 = tree.GetFromId(3);
  ASSERT_EQ(node3->parent(), node1);
  EXPECT_EQ(node3->parent()->parent(), nullptr);

  AXNode* node4 = tree.GetFromId(4);
  ASSERT_EQ(node4->parent(), node2);
  ASSERT_EQ(node4->parent()->parent(), node1);
  EXPECT_EQ(node4->parent()->parent()->parent(), nullptr);

  EXPECT_EQ(node1->GetLanguage(), "");
  EXPECT_EQ(node2->GetLanguage(), "");
  EXPECT_EQ(node3->GetLanguage(), "");
  EXPECT_EQ(node4->GetLanguage(), "");
}

TEST_F(AXLanguageDetectionTestStaticContent, Basic) {
  // Tree:
  //        1
  //      2   3
  //    4
  //  5
  //
  //  1 - German lang attribute,  no text
  //  2 - French lang attribute,  no text
  //  3 - no attribute,           French text
  //  4 - no attribute,           English text
  //  5 - no attribute,           no text
  //
  //  Expected:
  //    3 - French detected
  //    4 - English detected
  //    5 - inherit English from 4
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(5);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kGenericContainer;
    node2.child_ids.resize(1);
    node2.child_ids[0] = 4;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
  }

  {
    AXNodeData& node4 = initial_state.nodes[3];
    node4.id = 4;
    node4.child_ids.resize(1);
    node4.child_ids[0] = 5;
    node4.role = ax::mojom::Role::kStaticText;
    node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  {
    AXNodeData& node5 = initial_state.nodes[4];
    node5.id = 5;
    node5.role = ax::mojom::Role::kInlineTextBox;
    node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  {
    AXNode* node1 = tree.GetFromId(1);
    // node1 is not a text node, so no lang info should be attached.
    EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node1->GetLanguage(), "de");
  }

  {
    AXNode* node2 = tree.GetFromId(2);
    // node2 is not a text node, so no lang info should be attached.
    EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node2->GetLanguage(), "fr");
  }

  {
    AXNode* node3 = tree.GetFromId(3);
    EXPECT_TRUE(node3->IsText());
    EXPECT_NE(node3->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node3->GetLanguage(), "fr");
  }

  {
    AXNode* node4 = tree.GetFromId(4);
    EXPECT_TRUE(node4->IsText());
    EXPECT_NE(node4->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node4->GetLanguage(), "en");
  }

  {
    AXNode* node5 = tree.GetFromId(5);
    EXPECT_TRUE(node5->IsText());
    // Inherited languages are not stored in lang info.
    EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node5->GetLanguage(), "en");
  }
}

TEST_F(AXLanguageDetectionTestStaticContent, MetricCollection) {
  // Tree:
  //        1
  //    2 3 4 5 6
  //
  //  1 - German lang attribute,  no text
  //  2 - no attribute,           German text
  //  3 - no attribute,           French text
  //  4 - no attribute,           English text
  //  5 - no attribute,           Spanish text
  //  6 - no attribute,           text too short to get detection results.
  //
  //  Expected:
  //    2 - German detected
  //    3 - French detected
  //    4 - English detected
  //    5 - Spanish detected
  //    6 - too short for results
  //
  //    only 3 of these languages can be labelled due to heuristics.
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(6);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(5);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.child_ids[2] = 4;
    node1.child_ids[3] = 5;
    node1.child_ids[4] = 6;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
  }

  {
    AXNodeData& node4 = initial_state.nodes[3];
    node4.id = 4;
    node4.role = ax::mojom::Role::kStaticText;
    node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  {
    AXNodeData& node5 = initial_state.nodes[4];
    node5.id = 5;
    node5.role = ax::mojom::Role::kStaticText;
    node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextSpanish);
  }

  {
    AXNodeData& node6 = initial_state.nodes[5];
    node6.id = 6;
    node6.role = ax::mojom::Role::kStaticText;
    node6.AddStringAttribute(ax::mojom::StringAttribute::kName,
                             "too short for detection.");
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Specifically disable clearing of metrics.
  disable_metric_clearing(tree);
  // Our histogram for testing.
  base::HistogramTester histograms;

  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // All 4 of our languages should have been detected for one node each, scoring
  // a maximum 3 points.
  EXPECT_EQ(3, get_score(tree, "de"));
  EXPECT_EQ(3, get_score(tree, "en"));
  EXPECT_EQ(3, get_score(tree, "fr"));
  EXPECT_EQ(3, get_score(tree, "es"));

  // 5 nodes (2, 3, 4, 5, 6) should have had detection attempted.
  EXPECT_EQ(5, count_detection_attempted(tree));
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.CountDetectionAttempted", 5, 1);

  // 4 nodes (2, 3, 4, 5) should have had detection results.
  EXPECT_EQ(4, count_detection_results(tree));
  // 5 nodes attempted, 4 got results = 4*100/5 = 80%
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageLanguageDetected", 80, 1);

  // 3 nodes (any of 2, 3, 4, 5) should have been labelled.
  EXPECT_EQ(3, count_labelled(tree));
  histograms.ExpectUniqueSample("Accessibility.LanguageDetection.CountLabelled",
                                3, 1);

  // 3 nodes (any of 2, 3, 4, 5) should have been given top label.
  EXPECT_EQ(3, count_labelled_with_top_result(tree));
  // 3 nodes labelled, all of them given top result = 100%.
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageLabelledWithTop", 100, 1);

  // 3 nodes (3, 4, 5) should have been labelled to disagree with node1 author
  // provided language.
  EXPECT_EQ(3, count_overridden(tree));
  // 3 nodes labelled, all 3 disagree with node1 = 100%.
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageOverridden", 100, 1);

  // There should be 4 unique languages (de, en, fr, es).
  {
    const auto& top_lang = unique_top_lang_detected(tree);
    const absl::flat_hash_set<std::string> expected_top_lang = {"de", "en",
                                                                "es", "fr"};
    EXPECT_EQ(top_lang, expected_top_lang);
  }
  histograms.ExpectUniqueSample("Accessibility.LanguageDetection.LangsPerPage",
                                4, 1);
}

TEST_F(AXLanguageDetectionTestStaticContent, DetectOnly) {
  // This tests a Detect step without any matching Label step.
  //
  // Tree:
  //        1
  //      2   3
  //    4
  //  5
  //
  //  1 - German lang attribute, no text
  //  2 - French lang attribute,  no text
  //  3 - no attribute,           French text
  //  4 - no attribute,           English text
  //  5 - no attribute,           no text
  //
  //  Expected:
  //    3 - French detected, never labelled, so still inherits German from 1
  //    4 - English detected, never labelled, so still inherits French from 2
  //    5 - English inherited from 4, still inherits French from 4
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(5);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kGenericContainer;
    node2.child_ids.resize(1);
    node2.child_ids[0] = 4;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
  }

  {
    AXNodeData& node4 = initial_state.nodes[3];
    node4.id = 4;
    node4.child_ids.resize(1);
    node4.child_ids[0] = 5;
    node4.role = ax::mojom::Role::kStaticText;
    node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  {
    AXNodeData& node5 = initial_state.nodes[4];
    node5.id = 5;
    node5.role = ax::mojom::Role::kInlineTextBox;
    node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  // Purposefully not calling Label so we can test Detect in isolation.

  {
    AXNode* node1 = tree.GetFromId(1);
    // node1 is not a text node, so no lang info should be attached.
    EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node1->GetLanguage(), "de");
  }

  {
    AXNode* node2 = tree.GetFromId(2);
    // node2 is not a text node, so no lang info should be attached.
    EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node2->GetLanguage(), "fr");
  }

  {
    AXNode* node3 = tree.GetFromId(3);
    EXPECT_TRUE(node3->IsText());
    ASSERT_NE(node3->GetLanguageInfo(), nullptr);
    ASSERT_GT(node3->GetLanguageInfo()->detected_languages.size(), (unsigned)0);
    ASSERT_EQ(node3->GetLanguageInfo()->detected_languages[0], "fr");
    EXPECT_TRUE(node3->GetLanguageInfo()->language.empty());
    EXPECT_EQ(node3->GetLanguage(), "de");
  }

  {
    AXNode* node4 = tree.GetFromId(4);
    EXPECT_TRUE(node4->IsText());
    ASSERT_NE(node4->GetLanguageInfo(), nullptr);
    ASSERT_GT(node4->GetLanguageInfo()->detected_languages.size(), (unsigned)0);
    ASSERT_EQ(node4->GetLanguageInfo()->detected_languages[0], "en");
    EXPECT_TRUE(node4->GetLanguageInfo()->language.empty());
    EXPECT_EQ(node4->GetLanguage(), "fr");
  }

  {
    AXNode* node5 = tree.GetFromId(5);
    EXPECT_TRUE(node5->IsText());
    // Inherited languages are not stored in lang info.
    ASSERT_EQ(node5->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node5->GetLanguage(), "fr");
  }
}

TEST_F(AXLanguageDetectionTestStaticContent, kLanguageUntouched) {
  // This test is to ensure that the kLanguage string attribute is not updated
  // during language detection and labelling, even when it disagrees with the
  // detected language.

  // Built tree:
  //        1
  //      2   3
  //
  //  1 - German lang attribute,  no text
  //  2 - English lang attribute, French text
  //  3 - French lang attribute,  English text
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(3);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  {
    AXNode* node1 = tree.GetFromId(1);
    ASSERT_EQ(node1->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node1->GetLanguage(), "de");
  }

  {
    // French should be detected, original English attr should be untouched.
    AXNode* node2 = tree.GetFromId(2);
    ASSERT_NE(node2->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node2->GetLanguageInfo()->language, "fr");
    EXPECT_EQ(node2->GetStringAttribute(ax::mojom::StringAttribute::kLanguage),
              "en");
    EXPECT_EQ(node2->GetLanguage(), "fr");
  }

  {
    // English should be detected, original French attr should be untouched.
    AXNode* node3 = tree.GetFromId(3);
    ASSERT_NE(node3->GetLanguageInfo(), nullptr);
    EXPECT_EQ(node3->GetLanguageInfo()->language, "en");
    EXPECT_EQ(node3->GetStringAttribute(ax::mojom::StringAttribute::kLanguage),
              "fr");
    EXPECT_EQ(node3->GetLanguage(), "en");
  }
}

// Test RegisterLanguageDetectionObserver correctly respects the command line
// flags.
TEST_F(AXLanguageDetectionTestFixture, ObserverRegistrationObeysFlag) {
  // Enable only the flag controlling static language detection.
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);

  // Construct empty tree and check initialisation.
  AXTree tree;
  ASSERT_NE(tree.language_detection_manager, nullptr);
  ASSERT_EQ(getObserver(tree), nullptr);

  // Try registration without enabling Dynamic feature flag, should be a no-op.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();

  ASSERT_EQ(getObserver(tree), nullptr);

  // Now enable the dynamic feature flag.
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);

  // Try registration again, this should construct and register observer as flag
  // is now enabled.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();

  // Check our observer was constructed.
  ASSERT_NE(getObserver(tree), nullptr);

  // Check our observer was registered in our tree.
  ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
}

// Test RegisterLanguageDetectionObserver correctly respects the feature flag.
TEST_F(AXLanguageDetectionTestFixture, ObserverRegistrationObeysFeatureFlag) {
  // Construct empty tree and check initialisation.
  AXTree tree;
  ASSERT_NE(tree.language_detection_manager, nullptr);
  ASSERT_EQ(getObserver(tree), nullptr);

  // Try registration without enabling Dynamic feature flag, should be a no-op.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();

  ASSERT_EQ(getObserver(tree), nullptr);

  // Enable general feature flag which gates both Static and Dynamic features.
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitWithFeatures(
      {features::kEnableAccessibilityLanguageDetection}, {});

  // Try registration again, this should now construct and register an observer.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();

  // Check our observer was constructed.
  ASSERT_NE(getObserver(tree), nullptr);

  // Check our observer was registered in our tree.
  ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
}

TEST_F(AXLanguageDetectionTestDynamicContent, Basic) {
  // Tree:
  //   1
  //   2
  //
  // 1 - kStaticText - English text.
  // 2 - kInlineTextBox - English text.
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);

  // TODO(chrishall): Create more realistic kStaticText with multiple
  // kInlineTextBox(es) children. Look at the real-world behaviour of
  // kStaticText, kInlineText and kLineBreak around empty divs and empty lines
  // within paragraphs of text.

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kStaticText;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
    node1.child_ids.resize(1);
    node1.child_ids[0] = 2;
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kInlineTextBox;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Manually run initial language detection and labelling.
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Quickly verify "before" state
  {
    AXNode* node1 = tree.GetFromId(1);
    ASSERT_NE(node1, nullptr);
    ASSERT_NE(node1->GetLanguageInfo(), nullptr);
    ASSERT_EQ(node1->GetLanguage(), "en");

    AXNode* node2 = tree.GetFromId(2);
    ASSERT_NE(node2, nullptr);
    // Inherited language not stored in lang info.
    ASSERT_EQ(node2->GetLanguageInfo(), nullptr);
    // Should still inherit language from parent.
    ASSERT_EQ(node2->GetLanguage(), "en");
  }

  // Manually register observer.
  AXLanguageDetectionObserver observer(&tree);

  // Observer constructor is responsible for attaching itself to tree.
  ASSERT_TRUE(tree.HasObserver(&observer));

  // Dynamic update
  //
  // New tree:
  //     1
  //     2
  //
  // 1 - Text changed to German.
  // 2 - Text changed to German.
  AXTreeUpdate update_state;
  update_state.root_id = 1;
  update_state.nodes.resize(2);

  // Change text to German.
  {
    AXNodeData& node1 = update_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kStaticText;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
    node1.child_ids.resize(1);
    node1.child_ids[0] = 2;
  }

  {
    AXNodeData& node2 = update_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kInlineTextBox;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
  }

  // Perform update.
  ASSERT_TRUE(tree.Unserialize(update_state));

  // Check language detection was re-run on new content.
  {
    AXNode* node1 = tree.GetFromId(1);
    ASSERT_NE(node1, nullptr);
    ASSERT_NE(node1->GetLanguageInfo(), nullptr);
    ASSERT_EQ(node1->GetLanguage(), "de");
  }

  {
    AXNode* node2 = tree.GetFromId(2);
    ASSERT_NE(node2, nullptr);
    // Inherited language not stored in lang info.
    ASSERT_EQ(node2->GetLanguageInfo(), nullptr);
    // Should inherit new language from parent.
    ASSERT_EQ(node2->GetLanguage(), "de");
  }
}

TEST_F(AXLanguageDetectionTestDynamicContent, MetricCollection) {
  // Tree:
  //   1
  //  2 3
  //
  // 1 - kGenericContainer, French lang attribute.
  // 2 - kStaticText - English text.
  // 3 - kSTaticText - German text.
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(3);

  // TODO(chrishall): Create more realistic kStaticText with multiple
  // kInlineTextBox(es) children. Look at the real-world behaviour of
  // kStaticText, kInlineText and kLineBreak around empty divs and empty lines
  // within paragraphs of text.

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(2);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  {
    AXNodeData& node3 = initial_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Manually run initial language detection and labelling.
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Quickly verify "before" metrics were cleared.
  EXPECT_EQ(0, count_detection_attempted(tree));

  // Specifically disable clearing of metrics for dynamic only.
  disable_metric_clearing(tree);
  // Our histogram for testing.
  base::HistogramTester histograms;

  // Manually register observer.
  AXLanguageDetectionObserver observer(&tree);

  // Observer constructor is responsible for attaching itself to tree.
  ASSERT_TRUE(tree.HasObserver(&observer));

  // Dynamic update
  //
  // New tree:
  //     1
  //   2 3 4
  //
  // 1 - no change.
  // 2 - Text changed to French.
  // 3 - no change.
  // 4 - new kStaticText node, Spanish text.
  AXTreeUpdate update_state;
  update_state.root_id = 1;
  update_state.nodes.resize(3);

  // Change text to German.
  {
    AXNodeData& node1 = update_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(3);
    node1.child_ids[0] = 2;
    node1.child_ids[1] = 3;
    node1.child_ids[2] = 4;
    node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
  }

  {
    AXNodeData& node2 = update_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
  }

  {
    AXNodeData& node4 = update_state.nodes[2];
    node4.id = 4;
    node4.role = ax::mojom::Role::kStaticText;
    node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextSpanish);
  }

  // Perform update.
  ASSERT_TRUE(tree.Unserialize(update_state));

  // Check "after" metrics.
  // note that the metrics were cleared after static work had finished, so these
  // metrics only reflect the dynamic work.

  // All 4 of our languages should have been detected for one node each, scoring
  // a maximum 3 points.
  EXPECT_EQ(3, get_score(tree, "de"));
  EXPECT_EQ(3, get_score(tree, "en"));
  EXPECT_EQ(3, get_score(tree, "fr"));
  EXPECT_EQ(3, get_score(tree, "es"));

  // 2 nodes (2, 4) should have had detection attempted.
  EXPECT_EQ(2, count_detection_attempted(tree));
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.CountDetectionAttempted", 2, 1);

  // 2 nodes (2, 4) should have had detection results
  EXPECT_EQ(2, count_detection_results(tree));
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageLanguageDetected", 100, 1);

  // 2 nodes (2, 4) should have been labelled
  EXPECT_EQ(2, count_labelled(tree));
  histograms.ExpectUniqueSample("Accessibility.LanguageDetection.CountLabelled",
                                2, 1);

  // 2 nodes (2, 4) should have been given top label
  EXPECT_EQ(2, count_labelled_with_top_result(tree));
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageLabelledWithTop", 100, 1);

  // 1 nodes (4) should have been labelled to disagree with node1 author
  // provided language.
  EXPECT_EQ(1, count_overridden(tree));
  // 2 nodes were labelled, 1 disagreed with node1 = 50%.
  histograms.ExpectUniqueSample(
      "Accessibility.LanguageDetection.PercentageOverridden", 50, 1);

  // There should be 2 unique languages (fr, es).
  {
    auto top_lang = unique_top_lang_detected(tree);
    const absl::flat_hash_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.
  histograms.ExpectUniqueSample("Accessibility.LanguageDetection.LangsPerPage",
                                2, 1);
}

TEST_F(AXLanguageDetectionTestDynamicContent, MultipleUpdates) {
  // This test runs language detection a total of 3 times, once for the initial
  // 'static' content, and then twice for 'dynamic' updates.

  // Tree:
  //        1
  //        2
  //
  //  1 - GenericContainer
  //  2 - English text
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(1);
    node1.child_ids[0] = 2;
  }

  {
    AXNodeData& node2 = initial_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Run initial language detection and labelling.
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Quickly verify "before" state
  {
    AXNode* node1 = tree.GetFromId(1);
    ASSERT_NE(node1, nullptr);
    ASSERT_EQ(node1->GetLanguage(), "");

    AXNode* node2 = tree.GetFromId(2);
    ASSERT_NE(node2, nullptr);
    ASSERT_EQ(node2->GetLanguage(), "en");
  }

  // Register dynamic content observer.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();
  ASSERT_NE(getObserver(tree), nullptr);
  ASSERT_TRUE(tree.HasObserver(getObserver(tree)));

  // First update
  {
    // Dynamic update
    //
    // New tree layout will be:
    //        1
    //      2   3
    //
    //  1 - GenericContainer, unchanged
    //  2 - changed to German text
    //  3 - new child, French text
    AXTreeUpdate update_state;
    update_state.root_id = 1;
    update_state.nodes.resize(3);

    // Update node1 to include new child node3.
    {
      AXNodeData& node1 = update_state.nodes[0];
      node1.id = 1;
      node1.role = ax::mojom::Role::kGenericContainer;
      node1.child_ids.resize(2);
      node1.child_ids[0] = 2;
      node1.child_ids[1] = 3;
    }

    // Change node2 text to German
    {
      AXNodeData& node2 = update_state.nodes[1];
      node2.id = 2;
      node2.role = ax::mojom::Role::kStaticText;
      node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
    }

    // Add new node3 with French text.
    {
      AXNodeData& node3 = update_state.nodes[2];
      node3.id = 3;
      node3.role = ax::mojom::Role::kStaticText;
      node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
    }

    // Perform update.
    ASSERT_TRUE(tree.Unserialize(update_state));

    {
      AXNode* node1 = tree.GetFromId(1);
      ASSERT_NE(node1, nullptr);
      ASSERT_EQ(node1->GetLanguage(), "");
    }

    {
      // Detection should have been re-run on node2, detecting German.
      AXNode* node2 = tree.GetFromId(2);
      ASSERT_NE(node2, nullptr);
      ASSERT_EQ(node2->GetLanguage(), "de");
    }

    {
      // New node3 should have detected French.
      AXNode* node3 = tree.GetFromId(3);
      ASSERT_NE(node3, nullptr);
      ASSERT_EQ(node3->GetLanguage(), "fr");
    }
  }

  // Second update
  {
    // Dynamic update
    //
    // New tree layout will be:
    //        1
    //      2   x
    //
    //  1 - GenericContainer, unchanged
    //  2 - changed to French text
    //  3 - deleted
    AXTreeUpdate update_state;
    update_state.root_id = 1;
    update_state.nodes.resize(2);

    // Update node1 to delete child node3.
    {
      AXNodeData& node1 = update_state.nodes[0];
      node1.id = 1;
      node1.role = ax::mojom::Role::kGenericContainer;
      node1.child_ids.resize(1);
      node1.child_ids[0] = 2;
    }

    // Change node2 text to French
    {
      AXNodeData& node2 = update_state.nodes[1];
      node2.id = 2;
      node2.role = ax::mojom::Role::kStaticText;
      node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
    }

    // Perform update.
    ASSERT_TRUE(tree.Unserialize(update_state));

    {
      AXNode* node1 = tree.GetFromId(1);
      ASSERT_NE(node1, nullptr);
      ASSERT_EQ(node1->GetLanguage(), "");
    }

    {
      // Detection should have been re-run on node2, detecting French.
      AXNode* node2 = tree.GetFromId(2);
      ASSERT_NE(node2, nullptr);
      ASSERT_EQ(node2->GetLanguage(), "fr");
    }

    {
      // Node3 should be no more.
      AXNode* node3 = tree.GetFromId(3);
      ASSERT_EQ(node3, nullptr);
    }
  }

  // Third update.
  {
    // Dynamic update
    //
    // New tree layout will be:
    //        1
    //      2   3
    //
    //  1 - GenericContainer, unchanged
    //  2 - French text, unchanged
    //  3 - new node, German text
    AXTreeUpdate update_state;
    update_state.root_id = 1;
    update_state.nodes.resize(2);

    // Update node1 to include new child node3.
    {
      AXNodeData& node1 = update_state.nodes[0];
      node1.id = 1;
      node1.role = ax::mojom::Role::kGenericContainer;
      node1.child_ids.resize(2);
      node1.child_ids[0] = 2;
      node1.child_ids[1] = 3;
    }

    // Add new node3 with German text.
    {
      AXNodeData& node3 = update_state.nodes[1];
      node3.id = 3;
      node3.role = ax::mojom::Role::kStaticText;
      node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
    }

    // Perform update.
    ASSERT_TRUE(tree.Unserialize(update_state));

    {
      AXNode* node1 = tree.GetFromId(1);
      ASSERT_NE(node1, nullptr);
      ASSERT_EQ(node1->GetLanguage(), "");
    }

    {
      AXNode* node2 = tree.GetFromId(2);
      ASSERT_NE(node2, nullptr);
      ASSERT_EQ(node2->GetLanguage(), "fr");
    }

    {
      AXNode* node3 = tree.GetFromId(3);
      ASSERT_NE(node3, nullptr);
      ASSERT_EQ(node3->GetLanguage(), "de");
    }
  }
}

TEST_F(AXLanguageDetectionTestDynamicContent, NewRoot) {
  // Artificial test change which changes the root node.

  // Tree:
  //        1
  //
  //  1 - GenericContainer
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(1);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Run initial language detection and labelling.
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Register dynamic content observer.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();
  ASSERT_NE(getObserver(tree), nullptr);
  ASSERT_TRUE(tree.HasObserver(getObserver(tree)));

  // New Tree:
  //       2
  // 2 - new root, German text

  AXTreeUpdate update_state;
  update_state.root_id = 2;
  update_state.nodes.resize(1);

  {
    AXNodeData& node2 = update_state.nodes[0];
    node2.id = 2;
    node2.role = ax::mojom::Role::kStaticText;
    node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
  }

  // Perform update.
  ASSERT_TRUE(tree.Unserialize(update_state));

  {
    AXNode* node2 = tree.GetFromId(2);
    ASSERT_NE(node2, nullptr);
    ASSERT_EQ(node2->GetLanguage(), "de");
  }
}

TEST_F(AXLanguageDetectionTestDynamicContent, ChainOfNewNodes) {
  // Artificial test which adds two new nodes in a 'chain', simultaneously
  // adding a child of the root and a grandchild of the root.

  // Tree:
  //        1
  //
  //  1 - GenericContainer
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(1);

  {
    AXNodeData& node1 = initial_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
  }

  AXTree tree(initial_state);
  ASSERT_NE(tree.language_detection_manager, nullptr);

  // Run initial language detection and labelling.
  tree.language_detection_manager->DetectLanguages();
  tree.language_detection_manager->LabelLanguages();

  // Register dynamic content observer.
  tree.language_detection_manager->RegisterLanguageDetectionObserver();
  ASSERT_NE(getObserver(tree), nullptr);
  ASSERT_TRUE(tree.HasObserver(getObserver(tree)));

  // New Tree:
  //       1
  //       2
  //       3
  // 1 - generic container
  // 2 - generic container
  // 3 - German text

  AXTreeUpdate update_state;
  update_state.root_id = 1;
  update_state.nodes.resize(3);

  {
    AXNodeData& node1 = update_state.nodes[0];
    node1.id = 1;
    node1.role = ax::mojom::Role::kGenericContainer;
    node1.child_ids.resize(1);
    node1.child_ids[0] = 2;
  }

  {
    AXNodeData& node2 = update_state.nodes[1];
    node2.id = 2;
    node2.role = ax::mojom::Role::kGenericContainer;
    node2.child_ids.resize(1);
    node2.child_ids[0] = 3;
  }

  {
    AXNodeData& node3 = update_state.nodes[2];
    node3.id = 3;
    node3.role = ax::mojom::Role::kStaticText;
    node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
  }

  // Perform update.
  ASSERT_TRUE(tree.Unserialize(update_state));

  {
    AXNode* node3 = tree.GetFromId(3);
    ASSERT_NE(node3, nullptr);
    ASSERT_EQ(node3->GetLanguage(), "de");
  }
}

TEST(AXLanguageDetectionTest, AXLanguageInfoStatsBasic) {
  AXLanguageInfoStats stats;

  {
    std::vector<std::string> detected_languages;
    detected_languages.push_back("en");
    detected_languages.push_back("fr");
    detected_languages.push_back("ja");
    stats.Add(detected_languages);
  }

  ASSERT_EQ(stats.GetScore("en"), 3);
  ASSERT_EQ(stats.GetScore("fr"), 2);
  ASSERT_EQ(stats.GetScore("ja"), 1);

  EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("ja"));

  {
    std::vector<std::string> detected_languages;
    detected_languages.push_back("en");
    detected_languages.push_back("de");
    detected_languages.push_back("fr");
    stats.Add(detected_languages);
  }

  ASSERT_EQ(stats.GetScore("en"), 6);
  ASSERT_EQ(stats.GetScore("fr"), 3);
  ASSERT_EQ(stats.GetScore("de"), 2);
  ASSERT_EQ(stats.GetScore("ja"), 1);

  EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("de"));

  EXPECT_FALSE(stats.CheckLanguageWithinTop("ja"));

  {
    std::vector<std::string> detected_languages;
    detected_languages.push_back("fr");
    stats.Add(detected_languages);
  }

  ASSERT_EQ(stats.GetScore("en"), 6);
  ASSERT_EQ(stats.GetScore("fr"), 6);
  ASSERT_EQ(stats.GetScore("de"), 2);
  ASSERT_EQ(stats.GetScore("ja"), 1);

  EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("de"));

  EXPECT_FALSE(stats.CheckLanguageWithinTop("ja"));

  {
    std::vector<std::string> detected_languages;
    detected_languages.push_back("ja");
    detected_languages.push_back("qq");
    detected_languages.push_back("zz");
    stats.Add(detected_languages);
  }

  ASSERT_EQ(stats.GetScore("en"), 6);
  ASSERT_EQ(stats.GetScore("fr"), 6);
  ASSERT_EQ(stats.GetScore("ja"), 4);
  ASSERT_EQ(stats.GetScore("de"), 2);
  ASSERT_EQ(stats.GetScore("qq"), 2);
  ASSERT_EQ(stats.GetScore("zz"), 1);

  EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
  EXPECT_TRUE(stats.CheckLanguageWithinTop("ja"));

  EXPECT_FALSE(stats.CheckLanguageWithinTop("de"));
  EXPECT_FALSE(stats.CheckLanguageWithinTop("qq"));
  EXPECT_FALSE(stats.CheckLanguageWithinTop("zz"));
}

TEST(AXLanguageDetectionTest, ShortLanguageDetectorLabeledTest) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids = {2};
  initial_state.nodes[1].id = 2;
  initial_state.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                            "Hello");
  initial_state.nodes[1].AddStringAttribute(
      ax::mojom::StringAttribute::kLanguage, "en");
  AXTree tree(initial_state);

  AXNode* item = tree.GetFromId(2);
  std::vector<AXLanguageSpan> annotation;
  ASSERT_NE(tree.language_detection_manager, nullptr);
  // Empty output.
  annotation =
      tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
          *item, ax::mojom::StringAttribute::kMathContent);
  ASSERT_EQ(0, (int)annotation.size());
  // Returns single AXLanguageSpan.
  annotation =
      tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
          *item, ax::mojom::StringAttribute::kName);
  ASSERT_EQ(1, (int)annotation.size());
  AXLanguageSpan* lang_span = &annotation[0];
  ASSERT_EQ("en", lang_span->language);
  std::string name =
      item->GetStringAttribute(ax::mojom::StringAttribute::kName);
  ASSERT_EQ("Hello",
            name.substr(lang_span->start_index,
                        lang_span->end_index - lang_span->start_index));
}

TEST(AXLanguageDetectionTest, ShortLanguageDetectorCharacterTest) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids = {2};
  initial_state.nodes[1].id = 2;
  initial_state.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
                                            "δ");
  AXTree tree(initial_state);

  AXNode* item = tree.GetFromId(2);
  std::vector<AXLanguageSpan> annotation;
  ASSERT_NE(tree.language_detection_manager, nullptr);
  // Returns single LanguageSpan.
  annotation =
      tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
          *item, ax::mojom::StringAttribute::kName);
  ASSERT_EQ(1, (int)annotation.size());
  AXLanguageSpan* lang_span = &annotation[0];
  ASSERT_EQ("el", lang_span->language);
  std::string name =
      item->GetStringAttribute(ax::mojom::StringAttribute::kName);
  ASSERT_EQ("δ", name.substr(lang_span->start_index,
                             lang_span->end_index - lang_span->start_index));
}

TEST(AXLanguageDetectionTest, ShortLanguageDetectorMultipleLanguagesTest) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(2);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].child_ids = {2};
  initial_state.nodes[1].id = 2;
  initial_state.nodes[1].AddStringAttribute(
      ax::mojom::StringAttribute::kName,
      "This text should be read in English. 차에 한하여 중임할 수. Followed "
      "by English.");
  AXTree tree(initial_state);

  AXNode* item = tree.GetFromId(2);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  std::vector<AXLanguageSpan> annotation =
      tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
          *item, ax::mojom::StringAttribute::kName);
  ASSERT_EQ(3, (int)annotation.size());
  std::string name =
      item->GetStringAttribute(ax::mojom::StringAttribute::kName);
  AXLanguageSpan* lang_span = &annotation[0];
  ASSERT_EQ("This text should be read in English. ",
            name.substr(lang_span->start_index,
                        lang_span->end_index - lang_span->start_index));
  lang_span = &annotation[1];
  ASSERT_EQ("차에 한하여 중임할 수. ",
            name.substr(lang_span->start_index,
                        lang_span->end_index - lang_span->start_index));
  lang_span = &annotation[2];
  ASSERT_EQ("Followed by English.",
            name.substr(lang_span->start_index,
                        lang_span->end_index - lang_span->start_index));
}

// Assert that GetLanguageAnnotationForStringAttribute works for attributes
// other than kName.
TEST(AXLanguageDetectionTest, DetectLanguagesForRoleTest) {
  base::CommandLine::ForCurrentProcess()->AppendSwitch(
      ::switches::kEnableExperimentalAccessibilityLanguageDetection);
  AXTreeUpdate initial_state;
  initial_state.root_id = 1;
  initial_state.nodes.resize(1);
  initial_state.nodes[0].id = 1;
  initial_state.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kValue,
                                            "どうぞよろしくお願いします.");
  AXTree tree(initial_state);

  AXNode* item = tree.GetFromId(1);
  ASSERT_NE(tree.language_detection_manager, nullptr);
  std::vector<AXLanguageSpan> annotation =
      tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
          *item, ax::mojom::StringAttribute::kValue);
  ASSERT_EQ(1, (int)annotation.size());
  std::string value =
      item->GetStringAttribute(ax::mojom::StringAttribute::kValue);
  AXLanguageSpan* lang_span = &annotation[0];
  ASSERT_EQ("どうぞよろしくお願いします.",
            value.substr(lang_span->start_index,
                         lang_span->end_index - lang_span->start_index));
}

}  // namespace ui