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