1 // Copyright 2014 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_text_utils.h"
6 
7 #include <stddef.h>
8 #include <utility>
9 
10 #include "base/strings/utf_string_conversions.h"
11 #include "testing/gmock/include/gmock/gmock.h"
12 #include "testing/gtest/include/gtest/gtest.h"
13 #include "ui/accessibility/ax_enums.mojom.h"
14 
15 namespace ui {
16 
TEST(AXTextUtils,FindAccessibleTextBoundaryWord)17 TEST(AXTextUtils, FindAccessibleTextBoundaryWord) {
18   const base::string16 text =
19       base::UTF8ToUTF16("Hello there.This/is\ntesting.");
20   const size_t text_length = text.length();
21   std::vector<int> line_start_offsets;
22   line_start_offsets.push_back(19);
23   size_t result;
24 
25   result = FindAccessibleTextBoundary(
26       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 0,
27       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
28   EXPECT_EQ(6UL, result);
29   result = FindAccessibleTextBoundary(text, line_start_offsets,
30                                       ax::mojom::TextBoundary::kWordStart, 5,
31                                       ax::mojom::MoveDirection::kBackward,
32                                       ax::mojom::TextAffinity::kDownstream);
33   EXPECT_EQ(0UL, result);
34   result = FindAccessibleTextBoundary(
35       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 6,
36       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
37   EXPECT_EQ(12UL, result);
38   result = FindAccessibleTextBoundary(text, line_start_offsets,
39                                       ax::mojom::TextBoundary::kWordStart, 11,
40                                       ax::mojom::MoveDirection::kBackward,
41                                       ax::mojom::TextAffinity::kDownstream);
42   EXPECT_EQ(6UL, result);
43   result = FindAccessibleTextBoundary(text, line_start_offsets,
44                                       ax::mojom::TextBoundary::kWordStart, 12,
45                                       ax::mojom::MoveDirection::kBackward,
46                                       ax::mojom::TextAffinity::kDownstream);
47   EXPECT_EQ(12UL, result);
48   result = FindAccessibleTextBoundary(
49       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 15,
50       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
51   EXPECT_EQ(17UL, result);
52   result = FindAccessibleTextBoundary(text, line_start_offsets,
53                                       ax::mojom::TextBoundary::kWordStart, 15,
54                                       ax::mojom::MoveDirection::kBackward,
55                                       ax::mojom::TextAffinity::kDownstream);
56   EXPECT_EQ(12UL, result);
57   result = FindAccessibleTextBoundary(
58       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 16,
59       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
60   EXPECT_EQ(17UL, result);
61   result = FindAccessibleTextBoundary(
62       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 17,
63       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
64   EXPECT_EQ(20UL, result);
65   result = FindAccessibleTextBoundary(
66       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart, 20,
67       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
68   EXPECT_EQ(text_length, result);
69   result = FindAccessibleTextBoundary(
70       text, line_start_offsets, ax::mojom::TextBoundary::kWordStart,
71       text_length, ax::mojom::MoveDirection::kBackward,
72       ax::mojom::TextAffinity::kDownstream);
73   EXPECT_EQ(20UL, result);
74 }
75 
TEST(AXTextUtils,FindAccessibleTextBoundaryLine)76 TEST(AXTextUtils, FindAccessibleTextBoundaryLine) {
77   const base::string16 text = base::UTF8ToUTF16("Line 1.\nLine 2\n\t");
78   const size_t text_length = text.length();
79   std::vector<int> line_start_offsets;
80   line_start_offsets.push_back(8);
81   line_start_offsets.push_back(15);
82   size_t result;
83 
84   // Basic cases.
85   result = FindAccessibleTextBoundary(
86       text, line_start_offsets, ax::mojom::TextBoundary::kLineStart, 5,
87       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
88   EXPECT_EQ(8UL, result);
89   result = FindAccessibleTextBoundary(text, line_start_offsets,
90                                       ax::mojom::TextBoundary::kLineStart, 9,
91                                       ax::mojom::MoveDirection::kBackward,
92                                       ax::mojom::TextAffinity::kDownstream);
93   EXPECT_EQ(8UL, result);
94   result = FindAccessibleTextBoundary(
95       text, line_start_offsets, ax::mojom::TextBoundary::kLineStart, 10,
96       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
97   EXPECT_EQ(15UL, result);
98 
99   // Edge cases.
100   result = FindAccessibleTextBoundary(
101       text, line_start_offsets, ax::mojom::TextBoundary::kLineStart,
102       text_length, ax::mojom::MoveDirection::kBackward,
103       ax::mojom::TextAffinity::kDownstream);
104   EXPECT_EQ(15UL, result);
105 
106   // When the start_offset is at the start of the next line and we are searching
107   // backwards, it should not move.
108   result = FindAccessibleTextBoundary(text, line_start_offsets,
109                                       ax::mojom::TextBoundary::kLineStart, 15,
110                                       ax::mojom::MoveDirection::kBackward,
111                                       ax::mojom::TextAffinity::kDownstream);
112   EXPECT_EQ(15UL, result);
113 
114   // When the start_offset is at a hard line break and we are searching
115   // backwards, it should return the start of the previous line.
116   result = FindAccessibleTextBoundary(text, line_start_offsets,
117                                       ax::mojom::TextBoundary::kLineStart, 14,
118                                       ax::mojom::MoveDirection::kBackward,
119                                       ax::mojom::TextAffinity::kDownstream);
120   EXPECT_EQ(8UL, result);
121 
122   // When the start_offset is at the start of a line and we are searching
123   // forwards, it should return the start of the next line.
124   result = FindAccessibleTextBoundary(
125       text, line_start_offsets, ax::mojom::TextBoundary::kLineStart, 8,
126       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
127   EXPECT_EQ(15UL, result);
128 
129   // When there is no previous line break and we are searching backwards,
130   // it should return 0.
131   result = FindAccessibleTextBoundary(text, line_start_offsets,
132                                       ax::mojom::TextBoundary::kLineStart, 4,
133                                       ax::mojom::MoveDirection::kBackward,
134                                       ax::mojom::TextAffinity::kDownstream);
135   EXPECT_EQ(0UL, result);
136 
137   // When we are at the start of the last line and we are searching forwards.
138   // it should return the text length.
139   result = FindAccessibleTextBoundary(
140       text, line_start_offsets, ax::mojom::TextBoundary::kLineStart, 15,
141       ax::mojom::MoveDirection::kForward, ax::mojom::TextAffinity::kDownstream);
142   EXPECT_EQ(text_length, result);
143 }
144 
TEST(AXTextUtils,FindAccessibleTextBoundarySentence)145 TEST(AXTextUtils, FindAccessibleTextBoundarySentence) {
146   auto find_sentence_boundaries_at_offset = [](const base::string16& text,
147                                                int offset) {
148     std::vector<int> line_start_offsets;
149     size_t backwards = FindAccessibleTextBoundary(
150         text, line_start_offsets, ax::mojom::TextBoundary::kSentenceStart,
151         offset, ax::mojom::MoveDirection::kBackward,
152         ax::mojom::TextAffinity::kDownstream);
153     size_t forwards = FindAccessibleTextBoundary(
154         text, line_start_offsets, ax::mojom::TextBoundary::kSentenceStart,
155         offset, ax::mojom::MoveDirection::kForward,
156         ax::mojom::TextAffinity::kDownstream);
157     return std::make_pair(backwards, forwards);
158   };
159 
160   const base::string16 text =
161       base::UTF8ToUTF16("Sentence 1. Sentence 2...\n\tSentence 3! Sentence 4");
162   std::pair<size_t, size_t> boundaries =
163       find_sentence_boundaries_at_offset(text, 5);
164   EXPECT_EQ(0UL, boundaries.first);
165   EXPECT_EQ(12UL, boundaries.second);
166 
167   // When a sentence ends with multiple punctuation, we should look for the
168   // first word character that follows.
169   boundaries = find_sentence_boundaries_at_offset(text, 16);
170   EXPECT_EQ(12UL, boundaries.first);
171   EXPECT_EQ(27UL, boundaries.second);
172 
173   // This is also true if we start in the middle of that punctuation.
174   boundaries = find_sentence_boundaries_at_offset(text, 23);
175   EXPECT_EQ(12UL, boundaries.first);
176   EXPECT_EQ(27UL, boundaries.second);
177 
178   // When the offset is in the whitespace between two sentences, the boundaries
179   // should be those of the previous sentence to the beginning of the first
180   // non-whitespace character of the next one.
181   boundaries = find_sentence_boundaries_at_offset(text, 38);
182   EXPECT_EQ(27UL, boundaries.first);
183   EXPECT_EQ(39UL, boundaries.second);
184 
185   // The end of the string should be considered the end of the last sentence
186   // regardless of whether or not there is punctuation.
187   boundaries = find_sentence_boundaries_at_offset(text, 44);
188   EXPECT_EQ(39UL, boundaries.first);
189   EXPECT_EQ(49UL, boundaries.second);
190 
191   // The sentence should include whitespace all the way until the end of the
192   // string.
193   const base::string16 text2 = base::UTF8ToUTF16("A sentence . \n\n\t\t\n");
194   boundaries = find_sentence_boundaries_at_offset(text2, 10);
195   EXPECT_EQ(0UL, boundaries.first);
196   EXPECT_EQ(18UL, boundaries.second);
197 }
198 
TEST(AXTextUtils,FindAccessibleTextBoundaryCharacter)199 TEST(AXTextUtils, FindAccessibleTextBoundaryCharacter) {
200   static const wchar_t* kCharacters[] = {
201       // An English word consisting of four ASCII characters.
202       L"w",
203       L"o",
204       L"r",
205       L"d",
206       L" ",
207       // A Hindi word (which means "Hindi") consisting of three Devanagari
208       // characters.
209       L"\x0939\x093F",
210       L"\x0928\x094D",
211       L"\x0926\x0940",
212       L" ",
213       // A Thai word (which means "feel") consisting of three Thai characters.
214       L"\x0E23\x0E39\x0E49",
215       L"\x0E2A\x0E36",
216       L"\x0E01",
217       L" ",
218   };
219 
220   std::vector<base::string16> characters;
221   base::string16 text;
222   for (auto*& i : kCharacters) {
223     characters.push_back(base::WideToUTF16(i));
224     text.append(characters.back());
225   }
226 
227   auto verify_boundaries_at_offset = [&text](int offset, size_t start,
228                                              size_t end) {
229     testing::Message message;
230     message << "Testing character bounds at index " << offset;
231     SCOPED_TRACE(message);
232 
233     std::vector<int> line_start_offsets;
234     size_t backwards = FindAccessibleTextBoundary(
235         text, line_start_offsets, ax::mojom::TextBoundary::kCharacter, offset,
236         ax::mojom::MoveDirection::kBackward,
237         ax::mojom::TextAffinity::kDownstream);
238     EXPECT_EQ(backwards, start);
239 
240     size_t forwards = FindAccessibleTextBoundary(
241         text, line_start_offsets, ax::mojom::TextBoundary::kCharacter, offset,
242         ax::mojom::MoveDirection::kForward,
243         ax::mojom::TextAffinity::kDownstream);
244     EXPECT_EQ(forwards, end);
245   };
246 
247   verify_boundaries_at_offset(0, 0UL, 1UL);
248   verify_boundaries_at_offset(1, 1UL, 2UL);
249   verify_boundaries_at_offset(2, 2UL, 3UL);
250   verify_boundaries_at_offset(3, 3UL, 4UL);
251   verify_boundaries_at_offset(4, 4UL, 5UL);
252   verify_boundaries_at_offset(5, 5UL, 7UL);
253   verify_boundaries_at_offset(6, 5UL, 7UL);
254   verify_boundaries_at_offset(7, 7UL, 11UL);
255   verify_boundaries_at_offset(8, 7UL, 11UL);
256   verify_boundaries_at_offset(9, 7UL, 11UL);
257   verify_boundaries_at_offset(10, 7UL, 11UL);
258   verify_boundaries_at_offset(11, 11L, 12UL);
259   verify_boundaries_at_offset(12, 12L, 15UL);
260   verify_boundaries_at_offset(13, 12L, 15UL);
261   verify_boundaries_at_offset(14, 12L, 15UL);
262   verify_boundaries_at_offset(15, 15L, 17UL);
263   verify_boundaries_at_offset(16, 15L, 17UL);
264   verify_boundaries_at_offset(17, 17L, 18UL);
265   verify_boundaries_at_offset(18, 18L, 19UL);
266 }
267 
TEST(AXTextUtils,GetWordOffsetsEmptyTest)268 TEST(AXTextUtils, GetWordOffsetsEmptyTest) {
269   const base::string16 text = base::UTF8ToUTF16("");
270   std::vector<int> word_starts = GetWordStartOffsets(text);
271   std::vector<int> word_ends = GetWordEndOffsets(text);
272   EXPECT_EQ(0UL, word_starts.size());
273   EXPECT_EQ(0UL, word_ends.size());
274 }
275 
TEST(AXTextUtils,GetWordStartOffsetsBasicTest)276 TEST(AXTextUtils, GetWordStartOffsetsBasicTest) {
277   const base::string16 text = base::UTF8ToUTF16("This is very simple input");
278   EXPECT_THAT(GetWordStartOffsets(text), testing::ElementsAre(0, 5, 8, 13, 20));
279 }
280 
TEST(AXTextUtils,GetWordEndOffsetsBasicTest)281 TEST(AXTextUtils, GetWordEndOffsetsBasicTest) {
282   const base::string16 text = base::UTF8ToUTF16("This is very simple input");
283   EXPECT_THAT(GetWordEndOffsets(text), testing::ElementsAre(4, 7, 12, 19, 25));
284 }
285 
TEST(AXTextUtils,GetWordStartOffsetsMalformedInputTest)286 TEST(AXTextUtils, GetWordStartOffsetsMalformedInputTest) {
287   const base::string16 text =
288       base::UTF8ToUTF16("..we *## should parse $#@$ through bad ,,  input");
289   EXPECT_THAT(GetWordStartOffsets(text),
290               testing::ElementsAre(2, 9, 16, 27, 35, 43));
291 }
292 
293 }  // namespace ui
294