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