1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/accessibility/ax_language_detection.h"
6 
7 #include <stddef.h>
8 #include <stdint.h>
9 
10 #include <memory>
11 
12 #include "base/command_line.h"
13 #include "base/test/metrics/histogram_tester.h"
14 #include "base/test/scoped_feature_list.h"
15 #include "testing/gtest/include/gtest/gtest.h"
16 #include "ui/accessibility/accessibility_features.h"
17 #include "ui/accessibility/accessibility_switches.h"
18 #include "ui/accessibility/ax_enums.mojom.h"
19 #include "ui/accessibility/ax_node.h"
20 #include "ui/accessibility/ax_tree.h"
21 
22 namespace ui {
23 
24 const std::string kTextEnglish =
25     "This is text created using Google Translate, it is unlikely to be "
26     "idiomatic in the given target language. This text is only used to "
27     "test language detection";
28 
29 const std::string kTextFrench =
30     "Ce texte a été créé avec Google Translate, il est peu probable qu'il "
31     "soit idiomatique dans la langue cible indiquée Ce texte est "
32     "uniquement utilisé pour tester la détection de la langue.";
33 
34 const std::string kTextGerman =
35     "Dies ist ein mit Google Translate erstellter Text. Es ist "
36     "unwahrscheinlich, dass er in der angegebenen Zielsprache idiomatisch "
37     "ist. Dieser Text wird nur zum Testen der Spracherkennung verwendet.";
38 
39 const std::string kTextSpanish =
40     "Este es un texto creado usando Google Translate, es poco probable que sea "
41     "idiomático en el idioma de destino dado. Este texto solo se usa para "
42     "probar la detección de idioma.";
43 
44 // This test fixture is a friend of classes in ax_language_detection.h in order
45 // to enable testing of internals.
46 //
47 // When used with TEST_F, the test body is a subclass of this fixture, so we
48 // need to re-expose any members through this fixture in order for them to
49 // be accessible from within the test body.
50 class AXLanguageDetectionTestFixture : public testing::Test {
51  public:
52   AXLanguageDetectionTestFixture() = default;
53   ~AXLanguageDetectionTestFixture() override = default;
54 
55   AXLanguageDetectionTestFixture(const AXLanguageDetectionTestFixture&) =
56       delete;
57   AXLanguageDetectionTestFixture& operator=(
58       const AXLanguageDetectionTestFixture&) = delete;
59 
60  protected:
IsStaticLanguageDetectionEnabled()61   bool IsStaticLanguageDetectionEnabled() {
62     return AXLanguageDetectionManager::IsStaticLanguageDetectionEnabled();
63   }
64 
IsDynamicLanguageDetectionEnabled()65   bool IsDynamicLanguageDetectionEnabled() {
66     return AXLanguageDetectionManager::IsDynamicLanguageDetectionEnabled();
67   }
68 
getObserver(AXTree & tree)69   AXLanguageDetectionObserver* getObserver(AXTree& tree) {
70     return tree.language_detection_manager->language_detection_observer_.get();
71   }
72 
get_score(AXTree & tree,const std::string & lang)73   int get_score(AXTree& tree, const std::string& lang) {
74     return tree.language_detection_manager->lang_info_stats_.GetScore(lang);
75   }
76 
77   // Accessors for testing metric data.
disable_metric_clearing(AXTree & tree)78   void disable_metric_clearing(AXTree& tree) {
79     tree.language_detection_manager->lang_info_stats_.disable_metric_clearing_ =
80         true;
81   }
82 
count_detection_attempted(AXTree & tree) const83   int count_detection_attempted(AXTree& tree) const {
84     return tree.language_detection_manager->lang_info_stats_
85         .count_detection_attempted_;
86   }
87 
count_detection_results(AXTree & tree) const88   int count_detection_results(AXTree& tree) const {
89     return tree.language_detection_manager->lang_info_stats_
90         .count_detection_results_;
91   }
92 
count_labelled(AXTree & tree) const93   int count_labelled(AXTree& tree) const {
94     return tree.language_detection_manager->lang_info_stats_.count_labelled_;
95   }
96 
count_labelled_with_top_result(AXTree & tree) const97   int count_labelled_with_top_result(AXTree& tree) const {
98     return tree.language_detection_manager->lang_info_stats_
99         .count_labelled_with_top_result_;
100   }
101 
count_overridden(AXTree & tree) const102   int count_overridden(AXTree& tree) const {
103     return tree.language_detection_manager->lang_info_stats_.count_overridden_;
104   }
105 
unique_top_lang_detected(AXTree & tree) const106   const std::unordered_set<std::string>& unique_top_lang_detected(
107       AXTree& tree) const {
108     return tree.language_detection_manager->lang_info_stats_
109         .unique_top_lang_detected_;
110   }
111 };
112 
113 class AXLanguageDetectionTestStaticContent
114     : public AXLanguageDetectionTestFixture {
115  public:
116   AXLanguageDetectionTestStaticContent() = default;
117   ~AXLanguageDetectionTestStaticContent() override = default;
118 
119   AXLanguageDetectionTestStaticContent(
120       const AXLanguageDetectionTestStaticContent&) = delete;
121   AXLanguageDetectionTestStaticContent& operator=(
122       const AXLanguageDetectionTestStaticContent&) = delete;
123 
SetUp()124   void SetUp() override {
125     AXLanguageDetectionTestFixture::SetUp();
126 
127     base::CommandLine::ForCurrentProcess()->AppendSwitch(
128         ::switches::kEnableExperimentalAccessibilityLanguageDetection);
129   }
130 };
131 
132 class AXLanguageDetectionTestDynamicContent
133     : public AXLanguageDetectionTestStaticContent {
134  public:
135   AXLanguageDetectionTestDynamicContent() = default;
136   ~AXLanguageDetectionTestDynamicContent() override = default;
137 
138   AXLanguageDetectionTestDynamicContent(
139       const AXLanguageDetectionTestDynamicContent&) = delete;
140   AXLanguageDetectionTestDynamicContent& operator=(
141       const AXLanguageDetectionTestDynamicContent&) = delete;
142 
SetUp()143   void SetUp() override {
144     AXLanguageDetectionTestStaticContent::SetUp();
145 
146     base::CommandLine::ForCurrentProcess()->AppendSwitch(
147         ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);
148   }
149 };
150 
TEST_F(AXLanguageDetectionTestFixture,StaticContentFeatureFlag)151 TEST_F(AXLanguageDetectionTestFixture, StaticContentFeatureFlag) {
152   // TODO(crbug/889370): Remove this test once this feature is stable
153   EXPECT_FALSE(
154       ::switches::IsExperimentalAccessibilityLanguageDetectionEnabled());
155   EXPECT_FALSE(IsStaticLanguageDetectionEnabled());
156 
157   base::CommandLine::ForCurrentProcess()->AppendSwitch(
158       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
159 
160   EXPECT_TRUE(
161       ::switches::IsExperimentalAccessibilityLanguageDetectionEnabled());
162   EXPECT_TRUE(IsStaticLanguageDetectionEnabled());
163 }
164 
TEST_F(AXLanguageDetectionTestFixture,DynamicContentFeatureFlag)165 TEST_F(AXLanguageDetectionTestFixture, DynamicContentFeatureFlag) {
166   // TODO(crbug/889370): Remove this test once this feature is stable
167   EXPECT_FALSE(
168       ::switches::IsExperimentalAccessibilityLanguageDetectionDynamicEnabled());
169   EXPECT_FALSE(IsDynamicLanguageDetectionEnabled());
170 
171   base::CommandLine::ForCurrentProcess()->AppendSwitch(
172       ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);
173 
174   EXPECT_TRUE(
175       ::switches::IsExperimentalAccessibilityLanguageDetectionDynamicEnabled());
176   EXPECT_TRUE(IsDynamicLanguageDetectionEnabled());
177 }
178 
TEST_F(AXLanguageDetectionTestFixture,FeatureFlag)179 TEST_F(AXLanguageDetectionTestFixture, FeatureFlag) {
180   // TODO(crbug/889370): Remove this test once this feature is stable
181   EXPECT_FALSE(IsStaticLanguageDetectionEnabled());
182   EXPECT_FALSE(IsDynamicLanguageDetectionEnabled());
183 
184   base::test::ScopedFeatureList scoped_feature_list;
185   scoped_feature_list.InitWithFeatures(
186       {features::kEnableAccessibilityLanguageDetection}, {});
187 
188   EXPECT_TRUE(IsStaticLanguageDetectionEnabled());
189   EXPECT_TRUE(IsDynamicLanguageDetectionEnabled());
190 }
191 
TEST(AXLanguageDetectionTest,LangAttrInheritanceFeatureFlagOff)192 TEST(AXLanguageDetectionTest, LangAttrInheritanceFeatureFlagOff) {
193   // Test lang attribute inheritance when feature flag is off.
194   //
195   // Lang attribute inheritance is handled by GetLanguage.
196   //
197   // Tree:
198   //        1
199   //      2   3
200   //    4
201   //  5
202   //
203   //  1 - English lang attribute
204   //  2 - French lang attribute
205   //
206   //  Expected:
207   //    3 - inherit English from 1
208   //    4 - inherit French from 2
209   //    5 - inherit Frnech from 4/2
210   AXTreeUpdate initial_state;
211   initial_state.root_id = 1;
212   initial_state.nodes.resize(5);
213 
214   {
215     AXNodeData& node1 = initial_state.nodes[0];
216     node1.id = 1;
217     node1.role = ax::mojom::Role::kGenericContainer;
218     node1.child_ids.resize(2);
219     node1.child_ids[0] = 2;
220     node1.child_ids[1] = 3;
221     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
222   }
223 
224   {
225     AXNodeData& node2 = initial_state.nodes[1];
226     node2.id = 2;
227     node2.role = ax::mojom::Role::kGenericContainer;
228     node2.child_ids.resize(1);
229     node2.child_ids[0] = 4;
230     node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
231   }
232 
233   {
234     AXNodeData& node3 = initial_state.nodes[2];
235     node3.id = 3;
236     node3.role = ax::mojom::Role::kStaticText;
237   }
238 
239   {
240     AXNodeData& node4 = initial_state.nodes[3];
241     node4.id = 4;
242     node4.role = ax::mojom::Role::kStaticText;
243     node4.child_ids.resize(1);
244     node4.child_ids[0] = 5;
245   }
246 
247   {
248     AXNodeData& node5 = initial_state.nodes[4];
249     node5.id = 5;
250     node5.role = ax::mojom::Role::kInlineTextBox;
251   }
252 
253   AXTree tree(initial_state);
254   ASSERT_NE(tree.language_detection_manager, nullptr);
255   tree.language_detection_manager->DetectLanguages();
256   tree.language_detection_manager->LabelLanguages();
257 
258   {
259     AXNode* node1 = tree.GetFromId(1);
260     EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
261     EXPECT_EQ(node1->GetLanguage(), "en");
262   }
263 
264   {
265     AXNode* node2 = tree.GetFromId(2);
266     EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
267     EXPECT_EQ(node2->GetLanguage(), "fr");
268   }
269 
270   {
271     AXNode* node3 = tree.GetFromId(3);
272     EXPECT_EQ(node3->GetLanguageInfo(), nullptr);
273     EXPECT_EQ(node3->GetLanguage(), "en");
274   }
275 
276   {
277     AXNode* node4 = tree.GetFromId(4);
278     EXPECT_EQ(node4->GetLanguageInfo(), nullptr);
279     EXPECT_EQ(node4->GetLanguage(), "fr");
280   }
281 
282   {
283     AXNode* node5 = tree.GetFromId(5);
284     EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
285     EXPECT_EQ(node5->GetLanguage(), "fr");
286   }
287 }
288 
TEST(AXLanguageDetectionTest,LangAttrInheritanceFeatureFlagOn)289 TEST(AXLanguageDetectionTest, LangAttrInheritanceFeatureFlagOn) {
290   base::CommandLine::ForCurrentProcess()->AppendSwitch(
291       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
292 
293   // Test lang attribute inheritance in the absence of any detected language.
294   //
295   // Lang attribute inheritance is handled by the Label step.
296   //
297   // Tree:
298   //        1
299   //      2   3
300   //    4
301   //  5
302   //
303   //  1 - English lang attribute
304   //  2 - French lang attribute
305   //
306   //  Expected:
307   //    3 - inherit English from 1
308   //    4 - inherit French from 2
309   //    5 - inherit Frnech from 4/2
310 
311   AXTreeUpdate initial_state;
312   initial_state.root_id = 1;
313   initial_state.nodes.resize(5);
314 
315   {
316     AXNodeData& node1 = initial_state.nodes[0];
317     node1.id = 1;
318     node1.role = ax::mojom::Role::kGenericContainer;
319     node1.child_ids.resize(2);
320     node1.child_ids[0] = 2;
321     node1.child_ids[1] = 3;
322     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
323   }
324 
325   {
326     AXNodeData& node2 = initial_state.nodes[1];
327     node2.id = 2;
328     node2.role = ax::mojom::Role::kGenericContainer;
329     node2.child_ids.resize(1);
330     node2.child_ids[0] = 4;
331     node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
332   }
333 
334   {
335     AXNodeData& node3 = initial_state.nodes[2];
336     node3.id = 3;
337     node3.role = ax::mojom::Role::kStaticText;
338   }
339 
340   {
341     AXNodeData& node4 = initial_state.nodes[3];
342     node4.id = 4;
343     node4.role = ax::mojom::Role::kStaticText;
344     node4.child_ids.resize(1);
345     node4.child_ids[0] = 5;
346   }
347 
348   {
349     AXNodeData& node5 = initial_state.nodes[4];
350     node5.id = 5;
351     node5.role = ax::mojom::Role::kInlineTextBox;
352   }
353 
354   AXTree tree(initial_state);
355   ASSERT_NE(tree.language_detection_manager, nullptr);
356   tree.language_detection_manager->DetectLanguages();
357   tree.language_detection_manager->LabelLanguages();
358 
359   {
360     AXNode* node1 = tree.GetFromId(1);
361     // No detection for non text nodes.
362     EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
363     EXPECT_EQ(node1->GetLanguage(), "en");
364   }
365 
366   {
367     AXNode* node2 = tree.GetFromId(2);
368     EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
369     EXPECT_EQ(node2->GetLanguage(), "fr");
370   }
371 
372   {
373     AXNode* node3 = tree.GetFromId(3);
374     // Inherited languages are not stored in lang info.
375     EXPECT_EQ(node3->GetLanguageInfo(), nullptr);
376     EXPECT_EQ(node3->GetLanguage(), "en");
377   }
378 
379   {
380     AXNode* node4 = tree.GetFromId(4);
381     // Inherited languages are not stored in lang info.
382     EXPECT_EQ(node4->GetLanguageInfo(), nullptr);
383     EXPECT_EQ(node4->GetLanguage(), "fr");
384   }
385 
386   {
387     AXNode* node5 = tree.GetFromId(5);
388     // Inherited languages are not stored in lang info.
389     EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
390     EXPECT_EQ(node5->GetLanguage(), "fr");
391   }
392 }
393 
394 // Tests that AXNode::GetLanguage() terminates when there is no lang attribute.
TEST_F(AXLanguageDetectionTestStaticContent,GetLanguageBoringTree)395 TEST_F(AXLanguageDetectionTestStaticContent, GetLanguageBoringTree) {
396   // This test checks the behaviour of Detect, Label, and GetLanguage on a
397   // 'boring' tree.
398   //
399   // The tree built here contains no lang attributes, nor does it contain any
400   // text to perform detection on.
401   //
402   // Tree:
403   //      1
404   //    2   3
405   //  4
406   AXTreeUpdate initial_state;
407   initial_state.root_id = 1;
408   initial_state.nodes.resize(4);
409   initial_state.nodes[0].id = 1;
410   initial_state.nodes[0].child_ids.resize(2);
411   initial_state.nodes[0].child_ids[0] = 2;
412   initial_state.nodes[0].child_ids[1] = 3;
413   initial_state.nodes[1].id = 2;
414   initial_state.nodes[1].child_ids.resize(1);
415   initial_state.nodes[1].child_ids[0] = 4;
416   initial_state.nodes[2].id = 3;
417   initial_state.nodes[3].id = 4;
418 
419   AXTree tree(initial_state);
420   ASSERT_NE(tree.language_detection_manager, nullptr);
421   tree.language_detection_manager->DetectLanguages();
422   tree.language_detection_manager->LabelLanguages();
423 
424   // Check that tree parenting conforms to expected shape.
425   AXNode* node1 = tree.GetFromId(1);
426   EXPECT_EQ(node1->parent(), nullptr);
427 
428   AXNode* node2 = tree.GetFromId(2);
429   ASSERT_EQ(node2->parent(), node1);
430   EXPECT_EQ(node2->parent()->parent(), nullptr);
431 
432   AXNode* node3 = tree.GetFromId(3);
433   ASSERT_EQ(node3->parent(), node1);
434   EXPECT_EQ(node3->parent()->parent(), nullptr);
435 
436   AXNode* node4 = tree.GetFromId(4);
437   ASSERT_EQ(node4->parent(), node2);
438   ASSERT_EQ(node4->parent()->parent(), node1);
439   EXPECT_EQ(node4->parent()->parent()->parent(), nullptr);
440 
441   EXPECT_EQ(node1->GetLanguage(), "");
442   EXPECT_EQ(node2->GetLanguage(), "");
443   EXPECT_EQ(node3->GetLanguage(), "");
444   EXPECT_EQ(node4->GetLanguage(), "");
445 }
446 
TEST_F(AXLanguageDetectionTestStaticContent,Basic)447 TEST_F(AXLanguageDetectionTestStaticContent, Basic) {
448   // Tree:
449   //        1
450   //      2   3
451   //    4
452   //  5
453   //
454   //  1 - German lang attribute,  no text
455   //  2 - French lang attribute,  no text
456   //  3 - no attribute,           French text
457   //  4 - no attribute,           English text
458   //  5 - no attribute,           no text
459   //
460   //  Expected:
461   //    3 - French detected
462   //    4 - English detected
463   //    5 - inherit English from 4
464   AXTreeUpdate initial_state;
465   initial_state.root_id = 1;
466   initial_state.nodes.resize(5);
467 
468   {
469     AXNodeData& node1 = initial_state.nodes[0];
470     node1.id = 1;
471     node1.role = ax::mojom::Role::kGenericContainer;
472     node1.child_ids.resize(2);
473     node1.child_ids[0] = 2;
474     node1.child_ids[1] = 3;
475     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
476   }
477 
478   {
479     AXNodeData& node2 = initial_state.nodes[1];
480     node2.id = 2;
481     node2.role = ax::mojom::Role::kGenericContainer;
482     node2.child_ids.resize(1);
483     node2.child_ids[0] = 4;
484     node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
485   }
486 
487   {
488     AXNodeData& node3 = initial_state.nodes[2];
489     node3.id = 3;
490     node3.role = ax::mojom::Role::kStaticText;
491     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
492   }
493 
494   {
495     AXNodeData& node4 = initial_state.nodes[3];
496     node4.id = 4;
497     node4.child_ids.resize(1);
498     node4.child_ids[0] = 5;
499     node4.role = ax::mojom::Role::kStaticText;
500     node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
501   }
502 
503   {
504     AXNodeData& node5 = initial_state.nodes[4];
505     node5.id = 5;
506     node5.role = ax::mojom::Role::kInlineTextBox;
507     node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
508   }
509 
510   AXTree tree(initial_state);
511   ASSERT_NE(tree.language_detection_manager, nullptr);
512   tree.language_detection_manager->DetectLanguages();
513   tree.language_detection_manager->LabelLanguages();
514 
515   {
516     AXNode* node1 = tree.GetFromId(1);
517     // node1 is not a text node, so no lang info should be attached.
518     EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
519     EXPECT_EQ(node1->GetLanguage(), "de");
520   }
521 
522   {
523     AXNode* node2 = tree.GetFromId(2);
524     // node2 is not a text node, so no lang info should be attached.
525     EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
526     EXPECT_EQ(node2->GetLanguage(), "fr");
527   }
528 
529   {
530     AXNode* node3 = tree.GetFromId(3);
531     EXPECT_TRUE(node3->IsText());
532     EXPECT_NE(node3->GetLanguageInfo(), nullptr);
533     EXPECT_EQ(node3->GetLanguage(), "fr");
534   }
535 
536   {
537     AXNode* node4 = tree.GetFromId(4);
538     EXPECT_TRUE(node4->IsText());
539     EXPECT_NE(node4->GetLanguageInfo(), nullptr);
540     EXPECT_EQ(node4->GetLanguage(), "en");
541   }
542 
543   {
544     AXNode* node5 = tree.GetFromId(5);
545     EXPECT_TRUE(node5->IsText());
546     // Inherited languages are not stored in lang info.
547     EXPECT_EQ(node5->GetLanguageInfo(), nullptr);
548     EXPECT_EQ(node5->GetLanguage(), "en");
549   }
550 }
551 
TEST_F(AXLanguageDetectionTestStaticContent,MetricCollection)552 TEST_F(AXLanguageDetectionTestStaticContent, MetricCollection) {
553   // Tree:
554   //        1
555   //    2 3 4 5 6
556   //
557   //  1 - German lang attribute,  no text
558   //  2 - no attribute,           German text
559   //  3 - no attribute,           French text
560   //  4 - no attribute,           English text
561   //  5 - no attribute,           Spanish text
562   //  6 - no attribute,           text too short to get detection results.
563   //
564   //  Expected:
565   //    2 - German detected
566   //    3 - French detected
567   //    4 - English detected
568   //    5 - Spanish detected
569   //    6 - too short for results
570   //
571   //    only 3 of these languages can be labelled due to heuristics.
572   AXTreeUpdate initial_state;
573   initial_state.root_id = 1;
574   initial_state.nodes.resize(6);
575 
576   {
577     AXNodeData& node1 = initial_state.nodes[0];
578     node1.id = 1;
579     node1.role = ax::mojom::Role::kGenericContainer;
580     node1.child_ids.resize(5);
581     node1.child_ids[0] = 2;
582     node1.child_ids[1] = 3;
583     node1.child_ids[2] = 4;
584     node1.child_ids[3] = 5;
585     node1.child_ids[4] = 6;
586     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
587   }
588 
589   {
590     AXNodeData& node2 = initial_state.nodes[1];
591     node2.id = 2;
592     node2.role = ax::mojom::Role::kStaticText;
593     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
594   }
595 
596   {
597     AXNodeData& node3 = initial_state.nodes[2];
598     node3.id = 3;
599     node3.role = ax::mojom::Role::kStaticText;
600     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
601   }
602 
603   {
604     AXNodeData& node4 = initial_state.nodes[3];
605     node4.id = 4;
606     node4.role = ax::mojom::Role::kStaticText;
607     node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
608   }
609 
610   {
611     AXNodeData& node5 = initial_state.nodes[4];
612     node5.id = 5;
613     node5.role = ax::mojom::Role::kStaticText;
614     node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextSpanish);
615   }
616 
617   {
618     AXNodeData& node6 = initial_state.nodes[5];
619     node6.id = 6;
620     node6.role = ax::mojom::Role::kStaticText;
621     node6.AddStringAttribute(ax::mojom::StringAttribute::kName,
622                              "too short for detection.");
623   }
624 
625   AXTree tree(initial_state);
626   ASSERT_NE(tree.language_detection_manager, nullptr);
627 
628   // Specifically disable clearing of metrics.
629   disable_metric_clearing(tree);
630   // Our histogram for testing.
631   base::HistogramTester histograms;
632 
633   tree.language_detection_manager->DetectLanguages();
634   tree.language_detection_manager->LabelLanguages();
635 
636   // All 4 of our languages should have been detected for one node each, scoring
637   // a maximum 3 points.
638   EXPECT_EQ(3, get_score(tree, "de"));
639   EXPECT_EQ(3, get_score(tree, "en"));
640   EXPECT_EQ(3, get_score(tree, "fr"));
641   EXPECT_EQ(3, get_score(tree, "es"));
642 
643   // 5 nodes (2, 3, 4, 5, 6) should have had detection attempted.
644   EXPECT_EQ(5, count_detection_attempted(tree));
645   histograms.ExpectUniqueSample(
646       "Accessibility.LanguageDetection.CountDetectionAttempted", 5, 1);
647 
648   // 4 nodes (2, 3, 4, 5) should have had detection results.
649   EXPECT_EQ(4, count_detection_results(tree));
650   // 5 nodes attempted, 4 got results = 4*100/5 = 80%
651   histograms.ExpectUniqueSample(
652       "Accessibility.LanguageDetection.PercentageLanguageDetected", 80, 1);
653 
654   // 3 nodes (any of 2, 3, 4, 5) should have been labelled.
655   EXPECT_EQ(3, count_labelled(tree));
656   histograms.ExpectUniqueSample("Accessibility.LanguageDetection.CountLabelled",
657                                 3, 1);
658 
659   // 3 nodes (any of 2, 3, 4, 5) should have been given top label.
660   EXPECT_EQ(3, count_labelled_with_top_result(tree));
661   // 3 nodes labelled, all of them given top result = 100%.
662   histograms.ExpectUniqueSample(
663       "Accessibility.LanguageDetection.PercentageLabelledWithTop", 100, 1);
664 
665   // 3 nodes (3, 4, 5) should have been labelled to disagree with node1 author
666   // provided language.
667   EXPECT_EQ(3, count_overridden(tree));
668   // 3 nodes labelled, all 3 disagree with node1 = 100%.
669   histograms.ExpectUniqueSample(
670       "Accessibility.LanguageDetection.PercentageOverridden", 100, 1);
671 
672   // There should be 4 unique languages (de, en, fr, es).
673   {
674     const auto& top_lang = unique_top_lang_detected(tree);
675     const std::unordered_set<std::string> expected_top_lang = {"de", "en", "es",
676                                                                "fr"};
677     EXPECT_EQ(top_lang, expected_top_lang);
678   }
679   histograms.ExpectUniqueSample("Accessibility.LanguageDetection.LangsPerPage",
680                                 4, 1);
681 }
682 
TEST_F(AXLanguageDetectionTestStaticContent,DetectOnly)683 TEST_F(AXLanguageDetectionTestStaticContent, DetectOnly) {
684   // This tests a Detect step without any matching Label step.
685   //
686   // Tree:
687   //        1
688   //      2   3
689   //    4
690   //  5
691   //
692   //  1 - German lang attribute, no text
693   //  2 - French lang attribute,  no text
694   //  3 - no attribute,           French text
695   //  4 - no attribute,           English text
696   //  5 - no attribute,           no text
697   //
698   //  Expected:
699   //    3 - French detected, never labelled, so still inherits German from 1
700   //    4 - English detected, never labelled, so still inherits French from 2
701   //    5 - English inherited from 4, still inherits French from 4
702   AXTreeUpdate initial_state;
703   initial_state.root_id = 1;
704   initial_state.nodes.resize(5);
705 
706   {
707     AXNodeData& node1 = initial_state.nodes[0];
708     node1.id = 1;
709     node1.role = ax::mojom::Role::kGenericContainer;
710     node1.child_ids.resize(2);
711     node1.child_ids[0] = 2;
712     node1.child_ids[1] = 3;
713     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
714   }
715 
716   {
717     AXNodeData& node2 = initial_state.nodes[1];
718     node2.id = 2;
719     node2.role = ax::mojom::Role::kGenericContainer;
720     node2.child_ids.resize(1);
721     node2.child_ids[0] = 4;
722     node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
723   }
724 
725   {
726     AXNodeData& node3 = initial_state.nodes[2];
727     node3.id = 3;
728     node3.role = ax::mojom::Role::kStaticText;
729     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
730   }
731 
732   {
733     AXNodeData& node4 = initial_state.nodes[3];
734     node4.id = 4;
735     node4.child_ids.resize(1);
736     node4.child_ids[0] = 5;
737     node4.role = ax::mojom::Role::kStaticText;
738     node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
739   }
740 
741   {
742     AXNodeData& node5 = initial_state.nodes[4];
743     node5.id = 5;
744     node5.role = ax::mojom::Role::kInlineTextBox;
745     node5.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
746   }
747 
748   AXTree tree(initial_state);
749   ASSERT_NE(tree.language_detection_manager, nullptr);
750   tree.language_detection_manager->DetectLanguages();
751   // Purposefully not calling Label so we can test Detect in isolation.
752 
753   {
754     AXNode* node1 = tree.GetFromId(1);
755     // node1 is not a text node, so no lang info should be attached.
756     EXPECT_EQ(node1->GetLanguageInfo(), nullptr);
757     EXPECT_EQ(node1->GetLanguage(), "de");
758   }
759 
760   {
761     AXNode* node2 = tree.GetFromId(2);
762     // node2 is not a text node, so no lang info should be attached.
763     EXPECT_EQ(node2->GetLanguageInfo(), nullptr);
764     EXPECT_EQ(node2->GetLanguage(), "fr");
765   }
766 
767   {
768     AXNode* node3 = tree.GetFromId(3);
769     EXPECT_TRUE(node3->IsText());
770     ASSERT_NE(node3->GetLanguageInfo(), nullptr);
771     ASSERT_GT(node3->GetLanguageInfo()->detected_languages.size(), (unsigned)0);
772     ASSERT_EQ(node3->GetLanguageInfo()->detected_languages[0], "fr");
773     EXPECT_TRUE(node3->GetLanguageInfo()->language.empty());
774     EXPECT_EQ(node3->GetLanguage(), "de");
775   }
776 
777   {
778     AXNode* node4 = tree.GetFromId(4);
779     EXPECT_TRUE(node4->IsText());
780     ASSERT_NE(node4->GetLanguageInfo(), nullptr);
781     ASSERT_GT(node4->GetLanguageInfo()->detected_languages.size(), (unsigned)0);
782     ASSERT_EQ(node4->GetLanguageInfo()->detected_languages[0], "en");
783     EXPECT_TRUE(node4->GetLanguageInfo()->language.empty());
784     EXPECT_EQ(node4->GetLanguage(), "fr");
785   }
786 
787   {
788     AXNode* node5 = tree.GetFromId(5);
789     EXPECT_TRUE(node5->IsText());
790     // Inherited languages are not stored in lang info.
791     ASSERT_EQ(node5->GetLanguageInfo(), nullptr);
792     EXPECT_EQ(node5->GetLanguage(), "fr");
793   }
794 }
795 
TEST_F(AXLanguageDetectionTestStaticContent,kLanguageUntouched)796 TEST_F(AXLanguageDetectionTestStaticContent, kLanguageUntouched) {
797   // This test is to ensure that the kLanguage string attribute is not updated
798   // during language detection and labelling, even when it disagrees with the
799   // detected language.
800 
801   // Built tree:
802   //        1
803   //      2   3
804   //
805   //  1 - German lang attribute,  no text
806   //  2 - English lang attribute, French text
807   //  3 - French lang attribute,  English text
808   AXTreeUpdate initial_state;
809   initial_state.root_id = 1;
810   initial_state.nodes.resize(3);
811 
812   {
813     AXNodeData& node1 = initial_state.nodes[0];
814     node1.id = 1;
815     node1.role = ax::mojom::Role::kGenericContainer;
816     node1.child_ids.resize(2);
817     node1.child_ids[0] = 2;
818     node1.child_ids[1] = 3;
819     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "de");
820   }
821 
822   {
823     AXNodeData& node2 = initial_state.nodes[1];
824     node2.id = 2;
825     node2.role = ax::mojom::Role::kStaticText;
826     node2.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "en");
827     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
828   }
829 
830   {
831     AXNodeData& node3 = initial_state.nodes[2];
832     node3.id = 3;
833     node3.role = ax::mojom::Role::kStaticText;
834     node3.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
835     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
836   }
837 
838   AXTree tree(initial_state);
839   ASSERT_NE(tree.language_detection_manager, nullptr);
840   tree.language_detection_manager->DetectLanguages();
841   tree.language_detection_manager->LabelLanguages();
842 
843   {
844     AXNode* node1 = tree.GetFromId(1);
845     ASSERT_EQ(node1->GetLanguageInfo(), nullptr);
846     EXPECT_EQ(node1->GetLanguage(), "de");
847   }
848 
849   {
850     // French should be detected, original English attr should be untouched.
851     AXNode* node2 = tree.GetFromId(2);
852     ASSERT_NE(node2->GetLanguageInfo(), nullptr);
853     EXPECT_EQ(node2->GetLanguageInfo()->language, "fr");
854     EXPECT_EQ(node2->GetStringAttribute(ax::mojom::StringAttribute::kLanguage),
855               "en");
856     EXPECT_EQ(node2->GetLanguage(), "fr");
857   }
858 
859   {
860     // English should be detected, original French attr should be untouched.
861     AXNode* node3 = tree.GetFromId(3);
862     ASSERT_NE(node3->GetLanguageInfo(), nullptr);
863     EXPECT_EQ(node3->GetLanguageInfo()->language, "en");
864     EXPECT_EQ(node3->GetStringAttribute(ax::mojom::StringAttribute::kLanguage),
865               "fr");
866     EXPECT_EQ(node3->GetLanguage(), "en");
867   }
868 }
869 
870 // Test RegisterLanguageDetectionObserver correctly respects the command line
871 // flags.
TEST_F(AXLanguageDetectionTestFixture,ObserverRegistrationObeysFlag)872 TEST_F(AXLanguageDetectionTestFixture, ObserverRegistrationObeysFlag) {
873   // Enable only the flag controlling static language detection.
874   base::CommandLine::ForCurrentProcess()->AppendSwitch(
875       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
876 
877   // Construct empty tree and check initialisation.
878   AXTree tree;
879   ASSERT_NE(tree.language_detection_manager, nullptr);
880   ASSERT_EQ(getObserver(tree), nullptr);
881 
882   // Try registration without enabling Dynamic feature flag, should be a no-op.
883   tree.language_detection_manager->RegisterLanguageDetectionObserver();
884 
885   ASSERT_EQ(getObserver(tree), nullptr);
886 
887   // Now enable the dynamic feature flag.
888   base::CommandLine::ForCurrentProcess()->AppendSwitch(
889       ::switches::kEnableExperimentalAccessibilityLanguageDetectionDynamic);
890 
891   // Try registration again, this should construct and register observer as flag
892   // is now enabled.
893   tree.language_detection_manager->RegisterLanguageDetectionObserver();
894 
895   // Check our observer was constructed.
896   ASSERT_NE(getObserver(tree), nullptr);
897 
898   // Check our observer was registered in our tree.
899   ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
900 }
901 
902 // Test RegisterLanguageDetectionObserver correctly respects the feature flag.
TEST_F(AXLanguageDetectionTestFixture,ObserverRegistrationObeysFeatureFlag)903 TEST_F(AXLanguageDetectionTestFixture, ObserverRegistrationObeysFeatureFlag) {
904   // Construct empty tree and check initialisation.
905   AXTree tree;
906   ASSERT_NE(tree.language_detection_manager, nullptr);
907   ASSERT_EQ(getObserver(tree), nullptr);
908 
909   // Try registration without enabling Dynamic feature flag, should be a no-op.
910   tree.language_detection_manager->RegisterLanguageDetectionObserver();
911 
912   ASSERT_EQ(getObserver(tree), nullptr);
913 
914   // Enable general feature flag which gates both Static and Dynamic features.
915   base::test::ScopedFeatureList scoped_feature_list;
916   scoped_feature_list.InitWithFeatures(
917       {features::kEnableAccessibilityLanguageDetection}, {});
918 
919   // Try registration again, this should now construct and register an observer.
920   tree.language_detection_manager->RegisterLanguageDetectionObserver();
921 
922   // Check our observer was constructed.
923   ASSERT_NE(getObserver(tree), nullptr);
924 
925   // Check our observer was registered in our tree.
926   ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
927 }
928 
TEST_F(AXLanguageDetectionTestDynamicContent,Basic)929 TEST_F(AXLanguageDetectionTestDynamicContent, Basic) {
930   // Tree:
931   //   1
932   //   2
933   //
934   // 1 - kStaticText - English text.
935   // 2 - kInlineTextBox - English text.
936   AXTreeUpdate initial_state;
937   initial_state.root_id = 1;
938   initial_state.nodes.resize(2);
939 
940   // TODO(chrishall): Create more realistic kStaticText with multiple
941   // kInlineTextBox(es) children. Look at the real-world behaviour of
942   // kStaticText, kInlineText and kLineBreak around empty divs and empty lines
943   // within paragraphs of text.
944 
945   {
946     AXNodeData& node1 = initial_state.nodes[0];
947     node1.id = 1;
948     node1.role = ax::mojom::Role::kStaticText;
949     node1.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
950     node1.child_ids.resize(1);
951     node1.child_ids[0] = 2;
952   }
953 
954   {
955     AXNodeData& node2 = initial_state.nodes[1];
956     node2.id = 2;
957     node2.role = ax::mojom::Role::kInlineTextBox;
958     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
959   }
960 
961   AXTree tree(initial_state);
962   ASSERT_NE(tree.language_detection_manager, nullptr);
963 
964   // Manually run initial language detection and labelling.
965   tree.language_detection_manager->DetectLanguages();
966   tree.language_detection_manager->LabelLanguages();
967 
968   // Quickly verify "before" state
969   {
970     AXNode* node1 = tree.GetFromId(1);
971     ASSERT_NE(node1, nullptr);
972     ASSERT_NE(node1->GetLanguageInfo(), nullptr);
973     ASSERT_EQ(node1->GetLanguage(), "en");
974 
975     AXNode* node2 = tree.GetFromId(2);
976     ASSERT_NE(node2, nullptr);
977     // Inherited language not stored in lang info.
978     ASSERT_EQ(node2->GetLanguageInfo(), nullptr);
979     // Should still inherit language from parent.
980     ASSERT_EQ(node2->GetLanguage(), "en");
981   }
982 
983   // Manually register observer.
984   AXLanguageDetectionObserver observer(&tree);
985 
986   // Observer constructor is responsible for attaching itself to tree.
987   ASSERT_TRUE(tree.HasObserver(&observer));
988 
989   // Dynamic update
990   //
991   // New tree:
992   //     1
993   //     2
994   //
995   // 1 - Text changed to German.
996   // 2 - Text changed to German.
997   AXTreeUpdate update_state;
998   update_state.root_id = 1;
999   update_state.nodes.resize(2);
1000 
1001   // Change text to German.
1002   {
1003     AXNodeData& node1 = update_state.nodes[0];
1004     node1.id = 1;
1005     node1.role = ax::mojom::Role::kStaticText;
1006     node1.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1007     node1.child_ids.resize(1);
1008     node1.child_ids[0] = 2;
1009   }
1010 
1011   {
1012     AXNodeData& node2 = update_state.nodes[1];
1013     node2.id = 2;
1014     node2.role = ax::mojom::Role::kInlineTextBox;
1015     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1016   }
1017 
1018   // Perform update.
1019   ASSERT_TRUE(tree.Unserialize(update_state));
1020 
1021   // Check language detection was re-run on new content.
1022   {
1023     AXNode* node1 = tree.GetFromId(1);
1024     ASSERT_NE(node1, nullptr);
1025     ASSERT_NE(node1->GetLanguageInfo(), nullptr);
1026     ASSERT_EQ(node1->GetLanguage(), "de");
1027   }
1028 
1029   {
1030     AXNode* node2 = tree.GetFromId(2);
1031     ASSERT_NE(node2, nullptr);
1032     // Inherited language not stored in lang info.
1033     ASSERT_EQ(node2->GetLanguageInfo(), nullptr);
1034     // Should inherit new language from parent.
1035     ASSERT_EQ(node2->GetLanguage(), "de");
1036   }
1037 }
1038 
TEST_F(AXLanguageDetectionTestDynamicContent,MetricCollection)1039 TEST_F(AXLanguageDetectionTestDynamicContent, MetricCollection) {
1040   // Tree:
1041   //   1
1042   //  2 3
1043   //
1044   // 1 - kGenericContainer, French lang attribute.
1045   // 2 - kStaticText - English text.
1046   // 3 - kSTaticText - German text.
1047   AXTreeUpdate initial_state;
1048   initial_state.root_id = 1;
1049   initial_state.nodes.resize(3);
1050 
1051   // TODO(chrishall): Create more realistic kStaticText with multiple
1052   // kInlineTextBox(es) children. Look at the real-world behaviour of
1053   // kStaticText, kInlineText and kLineBreak around empty divs and empty lines
1054   // within paragraphs of text.
1055 
1056   {
1057     AXNodeData& node1 = initial_state.nodes[0];
1058     node1.id = 1;
1059     node1.role = ax::mojom::Role::kGenericContainer;
1060     node1.child_ids.resize(2);
1061     node1.child_ids[0] = 2;
1062     node1.child_ids[1] = 3;
1063     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
1064   }
1065 
1066   {
1067     AXNodeData& node2 = initial_state.nodes[1];
1068     node2.id = 2;
1069     node2.role = ax::mojom::Role::kStaticText;
1070     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
1071   }
1072 
1073   {
1074     AXNodeData& node3 = initial_state.nodes[2];
1075     node3.id = 3;
1076     node3.role = ax::mojom::Role::kStaticText;
1077     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1078   }
1079 
1080   AXTree tree(initial_state);
1081   ASSERT_NE(tree.language_detection_manager, nullptr);
1082 
1083   // Manually run initial language detection and labelling.
1084   tree.language_detection_manager->DetectLanguages();
1085   tree.language_detection_manager->LabelLanguages();
1086 
1087   // Quickly verify "before" metrics were cleared.
1088   EXPECT_EQ(0, count_detection_attempted(tree));
1089 
1090   // Specifically disable clearing of metrics for dynamic only.
1091   disable_metric_clearing(tree);
1092   // Our histogram for testing.
1093   base::HistogramTester histograms;
1094 
1095   // Manually register observer.
1096   AXLanguageDetectionObserver observer(&tree);
1097 
1098   // Observer constructor is responsible for attaching itself to tree.
1099   ASSERT_TRUE(tree.HasObserver(&observer));
1100 
1101   // Dynamic update
1102   //
1103   // New tree:
1104   //     1
1105   //   2 3 4
1106   //
1107   // 1 - no change.
1108   // 2 - Text changed to French.
1109   // 3 - no change.
1110   // 4 - new kStaticText node, Spanish text.
1111   AXTreeUpdate update_state;
1112   update_state.root_id = 1;
1113   update_state.nodes.resize(3);
1114 
1115   // Change text to German.
1116   {
1117     AXNodeData& node1 = update_state.nodes[0];
1118     node1.id = 1;
1119     node1.role = ax::mojom::Role::kGenericContainer;
1120     node1.child_ids.resize(3);
1121     node1.child_ids[0] = 2;
1122     node1.child_ids[1] = 3;
1123     node1.child_ids[2] = 4;
1124     node1.AddStringAttribute(ax::mojom::StringAttribute::kLanguage, "fr");
1125   }
1126 
1127   {
1128     AXNodeData& node2 = update_state.nodes[1];
1129     node2.id = 2;
1130     node2.role = ax::mojom::Role::kStaticText;
1131     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
1132   }
1133 
1134   {
1135     AXNodeData& node4 = update_state.nodes[2];
1136     node4.id = 4;
1137     node4.role = ax::mojom::Role::kStaticText;
1138     node4.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextSpanish);
1139   }
1140 
1141   // Perform update.
1142   ASSERT_TRUE(tree.Unserialize(update_state));
1143 
1144   // Check "after" metrics.
1145   // note that the metrics were cleared after static work had finished, so these
1146   // metrics only reflect the dynamic work.
1147 
1148   // All 4 of our languages should have been detected for one node each, scoring
1149   // a maximum 3 points.
1150   EXPECT_EQ(3, get_score(tree, "de"));
1151   EXPECT_EQ(3, get_score(tree, "en"));
1152   EXPECT_EQ(3, get_score(tree, "fr"));
1153   EXPECT_EQ(3, get_score(tree, "es"));
1154 
1155   // 2 nodes (2, 4) should have had detection attempted.
1156   EXPECT_EQ(2, count_detection_attempted(tree));
1157   histograms.ExpectUniqueSample(
1158       "Accessibility.LanguageDetection.CountDetectionAttempted", 2, 1);
1159 
1160   // 2 nodes (2, 4) should have had detection results
1161   EXPECT_EQ(2, count_detection_results(tree));
1162   histograms.ExpectUniqueSample(
1163       "Accessibility.LanguageDetection.PercentageLanguageDetected", 100, 1);
1164 
1165   // 2 nodes (2, 4) should have been labelled
1166   EXPECT_EQ(2, count_labelled(tree));
1167   histograms.ExpectUniqueSample("Accessibility.LanguageDetection.CountLabelled",
1168                                 2, 1);
1169 
1170   // 2 nodes (2, 4) should have been given top label
1171   EXPECT_EQ(2, count_labelled_with_top_result(tree));
1172   histograms.ExpectUniqueSample(
1173       "Accessibility.LanguageDetection.PercentageLabelledWithTop", 100, 1);
1174 
1175   // 1 nodes (4) should have been labelled to disagree with node1 author
1176   // provided language.
1177   EXPECT_EQ(1, count_overridden(tree));
1178   // 2 nodes were labelled, 1 disagreed with node1 = 50%.
1179   histograms.ExpectUniqueSample(
1180       "Accessibility.LanguageDetection.PercentageOverridden", 50, 1);
1181 
1182   // There should be 2 unique languages (fr, es).
1183   {
1184     auto top_lang = unique_top_lang_detected(tree);
1185     const std::unordered_set<std::string> expected_top_lang = {"es", "fr"};
1186     EXPECT_EQ(top_lang, expected_top_lang);
1187   }
1188   // There should be a single (unique, 1) value for '2' unique languages.
1189   histograms.ExpectUniqueSample("Accessibility.LanguageDetection.LangsPerPage",
1190                                 2, 1);
1191 }
1192 
TEST_F(AXLanguageDetectionTestDynamicContent,MultipleUpdates)1193 TEST_F(AXLanguageDetectionTestDynamicContent, MultipleUpdates) {
1194   // This test runs language detection a total of 3 times, once for the initial
1195   // 'static' content, and then twice for 'dynamic' updates.
1196 
1197   // Tree:
1198   //        1
1199   //        2
1200   //
1201   //  1 - GenericContainer
1202   //  2 - English text
1203   AXTreeUpdate initial_state;
1204   initial_state.root_id = 1;
1205   initial_state.nodes.resize(2);
1206 
1207   {
1208     AXNodeData& node1 = initial_state.nodes[0];
1209     node1.id = 1;
1210     node1.role = ax::mojom::Role::kGenericContainer;
1211     node1.child_ids.resize(1);
1212     node1.child_ids[0] = 2;
1213   }
1214 
1215   {
1216     AXNodeData& node2 = initial_state.nodes[1];
1217     node2.id = 2;
1218     node2.role = ax::mojom::Role::kStaticText;
1219     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextEnglish);
1220   }
1221 
1222   AXTree tree(initial_state);
1223   ASSERT_NE(tree.language_detection_manager, nullptr);
1224 
1225   // Run initial language detection and labelling.
1226   tree.language_detection_manager->DetectLanguages();
1227   tree.language_detection_manager->LabelLanguages();
1228 
1229   // Quickly verify "before" state
1230   {
1231     AXNode* node1 = tree.GetFromId(1);
1232     ASSERT_NE(node1, nullptr);
1233     ASSERT_EQ(node1->GetLanguage(), "");
1234 
1235     AXNode* node2 = tree.GetFromId(2);
1236     ASSERT_NE(node2, nullptr);
1237     ASSERT_EQ(node2->GetLanguage(), "en");
1238   }
1239 
1240   // Register dynamic content observer.
1241   tree.language_detection_manager->RegisterLanguageDetectionObserver();
1242   ASSERT_NE(getObserver(tree), nullptr);
1243   ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
1244 
1245   // First update
1246   {
1247     // Dynamic update
1248     //
1249     // New tree layout will be:
1250     //        1
1251     //      2   3
1252     //
1253     //  1 - GenericContainer, unchanged
1254     //  2 - changed to German text
1255     //  3 - new child, French text
1256     AXTreeUpdate update_state;
1257     update_state.root_id = 1;
1258     update_state.nodes.resize(3);
1259 
1260     // Update node1 to include new child node3.
1261     {
1262       AXNodeData& node1 = update_state.nodes[0];
1263       node1.id = 1;
1264       node1.role = ax::mojom::Role::kGenericContainer;
1265       node1.child_ids.resize(2);
1266       node1.child_ids[0] = 2;
1267       node1.child_ids[1] = 3;
1268     }
1269 
1270     // Change node2 text to German
1271     {
1272       AXNodeData& node2 = update_state.nodes[1];
1273       node2.id = 2;
1274       node2.role = ax::mojom::Role::kStaticText;
1275       node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1276     }
1277 
1278     // Add new node3 with French text.
1279     {
1280       AXNodeData& node3 = update_state.nodes[2];
1281       node3.id = 3;
1282       node3.role = ax::mojom::Role::kStaticText;
1283       node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
1284     }
1285 
1286     // Perform update.
1287     ASSERT_TRUE(tree.Unserialize(update_state));
1288 
1289     {
1290       AXNode* node1 = tree.GetFromId(1);
1291       ASSERT_NE(node1, nullptr);
1292       ASSERT_EQ(node1->GetLanguage(), "");
1293     }
1294 
1295     {
1296       // Detection should have been re-run on node2, detecting German.
1297       AXNode* node2 = tree.GetFromId(2);
1298       ASSERT_NE(node2, nullptr);
1299       ASSERT_EQ(node2->GetLanguage(), "de");
1300     }
1301 
1302     {
1303       // New node3 should have detected French.
1304       AXNode* node3 = tree.GetFromId(3);
1305       ASSERT_NE(node3, nullptr);
1306       ASSERT_EQ(node3->GetLanguage(), "fr");
1307     }
1308   }
1309 
1310   // Second update
1311   {
1312     // Dynamic update
1313     //
1314     // New tree layout will be:
1315     //        1
1316     //      2   x
1317     //
1318     //  1 - GenericContainer, unchanged
1319     //  2 - changed to French text
1320     //  3 - deleted
1321     AXTreeUpdate update_state;
1322     update_state.root_id = 1;
1323     update_state.nodes.resize(2);
1324 
1325     // Update node1 to delete child node3.
1326     {
1327       AXNodeData& node1 = update_state.nodes[0];
1328       node1.id = 1;
1329       node1.role = ax::mojom::Role::kGenericContainer;
1330       node1.child_ids.resize(1);
1331       node1.child_ids[0] = 2;
1332     }
1333 
1334     // Change node2 text to French
1335     {
1336       AXNodeData& node2 = update_state.nodes[1];
1337       node2.id = 2;
1338       node2.role = ax::mojom::Role::kStaticText;
1339       node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextFrench);
1340     }
1341 
1342     // Perform update.
1343     ASSERT_TRUE(tree.Unserialize(update_state));
1344 
1345     {
1346       AXNode* node1 = tree.GetFromId(1);
1347       ASSERT_NE(node1, nullptr);
1348       ASSERT_EQ(node1->GetLanguage(), "");
1349     }
1350 
1351     {
1352       // Detection should have been re-run on node2, detecting French.
1353       AXNode* node2 = tree.GetFromId(2);
1354       ASSERT_NE(node2, nullptr);
1355       ASSERT_EQ(node2->GetLanguage(), "fr");
1356     }
1357 
1358     {
1359       // Node3 should be no more.
1360       AXNode* node3 = tree.GetFromId(3);
1361       ASSERT_EQ(node3, nullptr);
1362     }
1363   }
1364 
1365   // Third update.
1366   {
1367     // Dynamic update
1368     //
1369     // New tree layout will be:
1370     //        1
1371     //      2   3
1372     //
1373     //  1 - GenericContainer, unchanged
1374     //  2 - French text, unchanged
1375     //  3 - new node, German text
1376     AXTreeUpdate update_state;
1377     update_state.root_id = 1;
1378     update_state.nodes.resize(2);
1379 
1380     // Update node1 to include new child node3.
1381     {
1382       AXNodeData& node1 = update_state.nodes[0];
1383       node1.id = 1;
1384       node1.role = ax::mojom::Role::kGenericContainer;
1385       node1.child_ids.resize(2);
1386       node1.child_ids[0] = 2;
1387       node1.child_ids[1] = 3;
1388     }
1389 
1390     // Add new node3 with German text.
1391     {
1392       AXNodeData& node3 = update_state.nodes[1];
1393       node3.id = 3;
1394       node3.role = ax::mojom::Role::kStaticText;
1395       node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1396     }
1397 
1398     // Perform update.
1399     ASSERT_TRUE(tree.Unserialize(update_state));
1400 
1401     {
1402       AXNode* node1 = tree.GetFromId(1);
1403       ASSERT_NE(node1, nullptr);
1404       ASSERT_EQ(node1->GetLanguage(), "");
1405     }
1406 
1407     {
1408       AXNode* node2 = tree.GetFromId(2);
1409       ASSERT_NE(node2, nullptr);
1410       ASSERT_EQ(node2->GetLanguage(), "fr");
1411     }
1412 
1413     {
1414       AXNode* node3 = tree.GetFromId(3);
1415       ASSERT_NE(node3, nullptr);
1416       ASSERT_EQ(node3->GetLanguage(), "de");
1417     }
1418   }
1419 }
1420 
TEST_F(AXLanguageDetectionTestDynamicContent,NewRoot)1421 TEST_F(AXLanguageDetectionTestDynamicContent, NewRoot) {
1422   // Artificial test change which changes the root node.
1423 
1424   // Tree:
1425   //        1
1426   //
1427   //  1 - GenericContainer
1428   AXTreeUpdate initial_state;
1429   initial_state.root_id = 1;
1430   initial_state.nodes.resize(1);
1431 
1432   {
1433     AXNodeData& node1 = initial_state.nodes[0];
1434     node1.id = 1;
1435     node1.role = ax::mojom::Role::kGenericContainer;
1436   }
1437 
1438   AXTree tree(initial_state);
1439   ASSERT_NE(tree.language_detection_manager, nullptr);
1440 
1441   // Run initial language detection and labelling.
1442   tree.language_detection_manager->DetectLanguages();
1443   tree.language_detection_manager->LabelLanguages();
1444 
1445   // Register dynamic content observer.
1446   tree.language_detection_manager->RegisterLanguageDetectionObserver();
1447   ASSERT_NE(getObserver(tree), nullptr);
1448   ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
1449 
1450   // New Tree:
1451   //       2
1452   // 2 - new root, German text
1453 
1454   AXTreeUpdate update_state;
1455   update_state.root_id = 2;
1456   update_state.nodes.resize(1);
1457 
1458   {
1459     AXNodeData& node2 = update_state.nodes[0];
1460     node2.id = 2;
1461     node2.role = ax::mojom::Role::kStaticText;
1462     node2.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1463   }
1464 
1465   // Perform update.
1466   ASSERT_TRUE(tree.Unserialize(update_state));
1467 
1468   {
1469     AXNode* node2 = tree.GetFromId(2);
1470     ASSERT_NE(node2, nullptr);
1471     ASSERT_EQ(node2->GetLanguage(), "de");
1472   }
1473 }
1474 
TEST_F(AXLanguageDetectionTestDynamicContent,ChainOfNewNodes)1475 TEST_F(AXLanguageDetectionTestDynamicContent, ChainOfNewNodes) {
1476   // Artificial test which adds two new nodes in a 'chain', simultaneously
1477   // adding a child of the root and a grandchild of the root.
1478 
1479   // Tree:
1480   //        1
1481   //
1482   //  1 - GenericContainer
1483   AXTreeUpdate initial_state;
1484   initial_state.root_id = 1;
1485   initial_state.nodes.resize(1);
1486 
1487   {
1488     AXNodeData& node1 = initial_state.nodes[0];
1489     node1.id = 1;
1490     node1.role = ax::mojom::Role::kGenericContainer;
1491   }
1492 
1493   AXTree tree(initial_state);
1494   ASSERT_NE(tree.language_detection_manager, nullptr);
1495 
1496   // Run initial language detection and labelling.
1497   tree.language_detection_manager->DetectLanguages();
1498   tree.language_detection_manager->LabelLanguages();
1499 
1500   // Register dynamic content observer.
1501   tree.language_detection_manager->RegisterLanguageDetectionObserver();
1502   ASSERT_NE(getObserver(tree), nullptr);
1503   ASSERT_TRUE(tree.HasObserver(getObserver(tree)));
1504 
1505   // New Tree:
1506   //       1
1507   //       2
1508   //       3
1509   // 1 - generic container
1510   // 2 - generic container
1511   // 3 - German text
1512 
1513   AXTreeUpdate update_state;
1514   update_state.root_id = 1;
1515   update_state.nodes.resize(3);
1516 
1517   {
1518     AXNodeData& node1 = update_state.nodes[0];
1519     node1.id = 1;
1520     node1.role = ax::mojom::Role::kGenericContainer;
1521     node1.child_ids.resize(1);
1522     node1.child_ids[0] = 2;
1523   }
1524 
1525   {
1526     AXNodeData& node2 = update_state.nodes[1];
1527     node2.id = 2;
1528     node2.role = ax::mojom::Role::kGenericContainer;
1529     node2.child_ids.resize(1);
1530     node2.child_ids[0] = 3;
1531   }
1532 
1533   {
1534     AXNodeData& node3 = update_state.nodes[2];
1535     node3.id = 3;
1536     node3.role = ax::mojom::Role::kStaticText;
1537     node3.AddStringAttribute(ax::mojom::StringAttribute::kName, kTextGerman);
1538   }
1539 
1540   // Perform update.
1541   ASSERT_TRUE(tree.Unserialize(update_state));
1542 
1543   {
1544     AXNode* node3 = tree.GetFromId(3);
1545     ASSERT_NE(node3, nullptr);
1546     ASSERT_EQ(node3->GetLanguage(), "de");
1547   }
1548 }
1549 
TEST(AXLanguageDetectionTest,AXLanguageInfoStatsBasic)1550 TEST(AXLanguageDetectionTest, AXLanguageInfoStatsBasic) {
1551   AXLanguageInfoStats stats;
1552 
1553   {
1554     std::vector<std::string> detected_languages;
1555     detected_languages.push_back("en");
1556     detected_languages.push_back("fr");
1557     detected_languages.push_back("ja");
1558     stats.Add(detected_languages);
1559   }
1560 
1561   ASSERT_EQ(stats.GetScore("en"), 3);
1562   ASSERT_EQ(stats.GetScore("fr"), 2);
1563   ASSERT_EQ(stats.GetScore("ja"), 1);
1564 
1565   EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
1566   EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
1567   EXPECT_TRUE(stats.CheckLanguageWithinTop("ja"));
1568 
1569   {
1570     std::vector<std::string> detected_languages;
1571     detected_languages.push_back("en");
1572     detected_languages.push_back("de");
1573     detected_languages.push_back("fr");
1574     stats.Add(detected_languages);
1575   }
1576 
1577   ASSERT_EQ(stats.GetScore("en"), 6);
1578   ASSERT_EQ(stats.GetScore("fr"), 3);
1579   ASSERT_EQ(stats.GetScore("de"), 2);
1580   ASSERT_EQ(stats.GetScore("ja"), 1);
1581 
1582   EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
1583   EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
1584   EXPECT_TRUE(stats.CheckLanguageWithinTop("de"));
1585 
1586   EXPECT_FALSE(stats.CheckLanguageWithinTop("ja"));
1587 
1588   {
1589     std::vector<std::string> detected_languages;
1590     detected_languages.push_back("fr");
1591     stats.Add(detected_languages);
1592   }
1593 
1594   ASSERT_EQ(stats.GetScore("en"), 6);
1595   ASSERT_EQ(stats.GetScore("fr"), 6);
1596   ASSERT_EQ(stats.GetScore("de"), 2);
1597   ASSERT_EQ(stats.GetScore("ja"), 1);
1598 
1599   EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
1600   EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
1601   EXPECT_TRUE(stats.CheckLanguageWithinTop("de"));
1602 
1603   EXPECT_FALSE(stats.CheckLanguageWithinTop("ja"));
1604 
1605   {
1606     std::vector<std::string> detected_languages;
1607     detected_languages.push_back("ja");
1608     detected_languages.push_back("qq");
1609     detected_languages.push_back("zz");
1610     stats.Add(detected_languages);
1611   }
1612 
1613   ASSERT_EQ(stats.GetScore("en"), 6);
1614   ASSERT_EQ(stats.GetScore("fr"), 6);
1615   ASSERT_EQ(stats.GetScore("ja"), 4);
1616   ASSERT_EQ(stats.GetScore("de"), 2);
1617   ASSERT_EQ(stats.GetScore("qq"), 2);
1618   ASSERT_EQ(stats.GetScore("zz"), 1);
1619 
1620   EXPECT_TRUE(stats.CheckLanguageWithinTop("en"));
1621   EXPECT_TRUE(stats.CheckLanguageWithinTop("fr"));
1622   EXPECT_TRUE(stats.CheckLanguageWithinTop("ja"));
1623 
1624   EXPECT_FALSE(stats.CheckLanguageWithinTop("de"));
1625   EXPECT_FALSE(stats.CheckLanguageWithinTop("qq"));
1626   EXPECT_FALSE(stats.CheckLanguageWithinTop("zz"));
1627 }
1628 
TEST(AXLanguageDetectionTest,ShortLanguageDetectorLabeledTest)1629 TEST(AXLanguageDetectionTest, ShortLanguageDetectorLabeledTest) {
1630   base::CommandLine::ForCurrentProcess()->AppendSwitch(
1631       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
1632   AXTreeUpdate initial_state;
1633   initial_state.root_id = 1;
1634   initial_state.nodes.resize(2);
1635   initial_state.nodes[0].id = 1;
1636   initial_state.nodes[0].child_ids = {2};
1637   initial_state.nodes[1].id = 2;
1638   initial_state.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
1639                                             "Hello");
1640   initial_state.nodes[1].AddStringAttribute(
1641       ax::mojom::StringAttribute::kLanguage, "en");
1642   AXTree tree(initial_state);
1643 
1644   AXNode* item = tree.GetFromId(2);
1645   std::vector<AXLanguageSpan> annotation;
1646   ASSERT_NE(tree.language_detection_manager, nullptr);
1647   // Empty output.
1648   annotation =
1649       tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
1650           *item, ax::mojom::StringAttribute::kInnerHtml);
1651   ASSERT_EQ(0, (int)annotation.size());
1652   // Returns single AXLanguageSpan.
1653   annotation =
1654       tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
1655           *item, ax::mojom::StringAttribute::kName);
1656   ASSERT_EQ(1, (int)annotation.size());
1657   AXLanguageSpan* lang_span = &annotation[0];
1658   ASSERT_EQ("en", lang_span->language);
1659   std::string name =
1660       item->GetStringAttribute(ax::mojom::StringAttribute::kName);
1661   ASSERT_EQ("Hello",
1662             name.substr(lang_span->start_index,
1663                         lang_span->end_index - lang_span->start_index));
1664 }
1665 
TEST(AXLanguageDetectionTest,ShortLanguageDetectorCharacterTest)1666 TEST(AXLanguageDetectionTest, ShortLanguageDetectorCharacterTest) {
1667   base::CommandLine::ForCurrentProcess()->AppendSwitch(
1668       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
1669   AXTreeUpdate initial_state;
1670   initial_state.root_id = 1;
1671   initial_state.nodes.resize(2);
1672   initial_state.nodes[0].id = 1;
1673   initial_state.nodes[0].child_ids = {2};
1674   initial_state.nodes[1].id = 2;
1675   initial_state.nodes[1].AddStringAttribute(ax::mojom::StringAttribute::kName,
1676                                             "δ");
1677   AXTree tree(initial_state);
1678 
1679   AXNode* item = tree.GetFromId(2);
1680   std::vector<AXLanguageSpan> annotation;
1681   ASSERT_NE(tree.language_detection_manager, nullptr);
1682   // Returns single LanguageSpan.
1683   annotation =
1684       tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
1685           *item, ax::mojom::StringAttribute::kName);
1686   ASSERT_EQ(1, (int)annotation.size());
1687   AXLanguageSpan* lang_span = &annotation[0];
1688   ASSERT_EQ("el", lang_span->language);
1689   std::string name =
1690       item->GetStringAttribute(ax::mojom::StringAttribute::kName);
1691   ASSERT_EQ("δ", name.substr(lang_span->start_index,
1692                              lang_span->end_index - lang_span->start_index));
1693 }
1694 
TEST(AXLanguageDetectionTest,ShortLanguageDetectorMultipleLanguagesTest)1695 TEST(AXLanguageDetectionTest, ShortLanguageDetectorMultipleLanguagesTest) {
1696   base::CommandLine::ForCurrentProcess()->AppendSwitch(
1697       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
1698   AXTreeUpdate initial_state;
1699   initial_state.root_id = 1;
1700   initial_state.nodes.resize(2);
1701   initial_state.nodes[0].id = 1;
1702   initial_state.nodes[0].child_ids = {2};
1703   initial_state.nodes[1].id = 2;
1704   initial_state.nodes[1].AddStringAttribute(
1705       ax::mojom::StringAttribute::kName,
1706       "This text should be read in English. 차에 한하여 중임할 수. Followed "
1707       "by English.");
1708   AXTree tree(initial_state);
1709 
1710   AXNode* item = tree.GetFromId(2);
1711   ASSERT_NE(tree.language_detection_manager, nullptr);
1712   std::vector<AXLanguageSpan> annotation =
1713       tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
1714           *item, ax::mojom::StringAttribute::kName);
1715   ASSERT_EQ(3, (int)annotation.size());
1716   std::string name =
1717       item->GetStringAttribute(ax::mojom::StringAttribute::kName);
1718   AXLanguageSpan* lang_span = &annotation[0];
1719   ASSERT_EQ("This text should be read in English. ",
1720             name.substr(lang_span->start_index,
1721                         lang_span->end_index - lang_span->start_index));
1722   lang_span = &annotation[1];
1723   ASSERT_EQ("차에 한하여 중임할 수. ",
1724             name.substr(lang_span->start_index,
1725                         lang_span->end_index - lang_span->start_index));
1726   lang_span = &annotation[2];
1727   ASSERT_EQ("Followed by English.",
1728             name.substr(lang_span->start_index,
1729                         lang_span->end_index - lang_span->start_index));
1730 }
1731 
1732 // Assert that GetLanguageAnnotationForStringAttribute works for attributes
1733 // other than kName.
TEST(AXLanguageDetectionTest,DetectLanguagesForRoleTest)1734 TEST(AXLanguageDetectionTest, DetectLanguagesForRoleTest) {
1735   base::CommandLine::ForCurrentProcess()->AppendSwitch(
1736       ::switches::kEnableExperimentalAccessibilityLanguageDetection);
1737   AXTreeUpdate initial_state;
1738   initial_state.root_id = 1;
1739   initial_state.nodes.resize(1);
1740   initial_state.nodes[0].id = 1;
1741   initial_state.nodes[0].AddStringAttribute(ax::mojom::StringAttribute::kValue,
1742                                             "どうぞよろしくお願いします.");
1743   AXTree tree(initial_state);
1744 
1745   AXNode* item = tree.GetFromId(1);
1746   ASSERT_NE(tree.language_detection_manager, nullptr);
1747   std::vector<AXLanguageSpan> annotation =
1748       tree.language_detection_manager->GetLanguageAnnotationForStringAttribute(
1749           *item, ax::mojom::StringAttribute::kValue);
1750   ASSERT_EQ(1, (int)annotation.size());
1751   std::string value =
1752       item->GetStringAttribute(ax::mojom::StringAttribute::kValue);
1753   AXLanguageSpan* lang_span = &annotation[0];
1754   ASSERT_EQ("どうぞよろしくお願いします.",
1755             value.substr(lang_span->start_index,
1756                          lang_span->end_index - lang_span->start_index));
1757 }
1758 
1759 }  // namespace ui
1760