Flutter Linux Embedder
fl_accessible_text_field_test.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter 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 // Included first as it collides with the X11 headers.
6 #include "flutter/shell/platform/linux/testing/linux_test.h"
7 #include "gtest/gtest.h"
8 
9 #include "flutter/shell/platform/embedder/test_utils/proc_table_replacement.h"
13 #include "flutter/shell/platform/linux/testing/mock_signal_handler.h"
14 
15 // MOCK_ENGINE_PROC is leaky by design
16 // NOLINTBEGIN(clang-analyzer-core.StackAddressEscape)
17 
18 static FlValue* decode_semantic_data(const uint8_t* data, size_t data_length) {
19  g_autoptr(GBytes) bytes = g_bytes_new(data, data_length);
20  g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
21  return fl_message_codec_decode_message(FL_MESSAGE_CODEC(codec), bytes,
22  nullptr);
23 }
24 
25 class FlAccessibleTextFieldTest : public flutter::testing::LinuxTest {};
26 
27 // Tests that semantic node value updates from Flutter emit AtkText::text-insert
28 // and AtkText::text-remove signals as expected.
30  g_autoptr(FlAccessibleNode) node =
31  fl_accessible_text_field_new(engine, 123, 1);
32 
33  // "" -> "Flutter"
34  {
35  flutter::testing::MockSignalHandler2<int, int> text_inserted(node,
36  "text-insert");
37  flutter::testing::MockSignalHandler text_removed(node, "text-remove");
38 
39  EXPECT_SIGNAL2(text_inserted, ::testing::Eq(0), ::testing::Eq(7));
40  EXPECT_SIGNAL(text_removed).Times(0);
41 
42  fl_accessible_node_set_value(node, "Flutter");
43  }
44 
45  // "Flutter" -> "Flutter"
46  {
47  flutter::testing::MockSignalHandler text_inserted(node, "text-insert");
48  flutter::testing::MockSignalHandler text_removed(node, "text-remove");
49 
50  EXPECT_SIGNAL(text_inserted).Times(0);
51  EXPECT_SIGNAL(text_removed).Times(0);
52 
53  fl_accessible_node_set_value(node, "Flutter");
54  }
55 
56  // "Flutter" -> "engine"
57  {
58  flutter::testing::MockSignalHandler2<int, int> text_inserted(node,
59  "text-insert");
60  flutter::testing::MockSignalHandler2<int, int> text_removed(node,
61  "text-remove");
62 
63  EXPECT_SIGNAL2(text_inserted, ::testing::Eq(0), ::testing::Eq(6));
64  EXPECT_SIGNAL2(text_removed, ::testing::Eq(0), ::testing::Eq(7));
65 
66  fl_accessible_node_set_value(node, "engine");
67  }
68 
69  // "engine" -> ""
70  {
71  flutter::testing::MockSignalHandler text_inserted(node, "text-insert");
72  flutter::testing::MockSignalHandler2<int, int> text_removed(node,
73  "text-remove");
74 
75  EXPECT_SIGNAL(text_inserted).Times(0);
76  EXPECT_SIGNAL2(text_removed, ::testing::Eq(0), ::testing::Eq(6));
77 
79  }
80 }
81 
82 // Tests that semantic node selection updates from Flutter emit
83 // AtkText::text-selection-changed and AtkText::text-caret-moved signals as
84 // expected.
85 TEST_F(FlAccessibleTextFieldTest, SetTextSelection) {
86  g_autoptr(FlAccessibleNode) node =
87  fl_accessible_text_field_new(engine, 123, 1);
88 
89  // [-1,-1] -> [2,3]
90  {
91  flutter::testing::MockSignalHandler text_selection_changed(
92  node, "text-selection-changed");
93  flutter::testing::MockSignalHandler1<int> text_caret_moved(
94  node, "text-caret-moved");
95 
96  EXPECT_SIGNAL(text_selection_changed);
97  EXPECT_SIGNAL1(text_caret_moved, ::testing::Eq(3));
98 
100  }
101 
102  // [2,3] -> [3,3]
103  {
104  flutter::testing::MockSignalHandler text_selection_changed(
105  node, "text-selection-changed");
106  flutter::testing::MockSignalHandler text_caret_moved(node,
107  "text-caret-moved");
108 
109  EXPECT_SIGNAL(text_selection_changed);
110  EXPECT_SIGNAL(text_caret_moved).Times(0);
111 
113  }
114 
115  // [3,3] -> [3,3]
116  {
117  flutter::testing::MockSignalHandler text_selection_changed(
118  node, "text-selection-changed");
119  flutter::testing::MockSignalHandler text_caret_moved(node,
120  "text-caret-moved");
121 
122  EXPECT_SIGNAL(text_selection_changed).Times(0);
123  EXPECT_SIGNAL(text_caret_moved).Times(0);
124 
126  }
127 
128  // [3,3] -> [4,4]
129  {
130  flutter::testing::MockSignalHandler text_selection_changed(
131  node, "text-selection-changed");
132  flutter::testing::MockSignalHandler1<int> text_caret_moved(
133  node, "text-caret-moved");
134 
135  EXPECT_SIGNAL(text_selection_changed).Times(0);
136  EXPECT_SIGNAL1(text_caret_moved, ::testing::Eq(4));
137 
139  }
140 }
141 
142 // Tests that fl_accessible_text_field_perform_action() passes the required
143 // "expandSelection" argument for semantic cursor move actions.
145  g_autoptr(GPtrArray) action_datas = g_ptr_array_new_with_free_func(
146  reinterpret_cast<GDestroyNotify>(fl_value_unref));
147 
148  StartEngine();
149 
150  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
151  SendSemanticsAction,
152  ([&action_datas](auto engine,
153  const FlutterSendSemanticsActionInfo* info) {
154  g_ptr_array_add(action_datas,
155  decode_semantic_data(info->data, info->data_length));
156  return kSuccess;
157  }));
158 
159  g_autoptr(FlAccessibleNode) node =
160  fl_accessible_text_field_new(engine, 123, 1);
162  node, static_cast<FlutterSemanticsAction>(
163  kFlutterSemanticsActionMoveCursorForwardByCharacter |
164  kFlutterSemanticsActionMoveCursorBackwardByCharacter |
165  kFlutterSemanticsActionMoveCursorForwardByWord |
166  kFlutterSemanticsActionMoveCursorBackwardByWord));
167 
168  g_autoptr(FlValue) expand_selection = fl_value_new_bool(false);
169 
170  for (int i = 0; i < 4; ++i) {
171  atk_action_do_action(ATK_ACTION(node), i);
172 
173  FlValue* data = static_cast<FlValue*>(g_ptr_array_index(action_datas, i));
174  EXPECT_NE(data, nullptr);
175  EXPECT_TRUE(fl_value_equal(data, expand_selection));
176  }
177 }
178 
179 // Tests AtkText::get_character_count.
180 TEST_F(FlAccessibleTextFieldTest, GetCharacterCount) {
181  g_autoptr(FlAccessibleNode) node =
182  fl_accessible_text_field_new(engine, 123, 1);
183 
184  EXPECT_EQ(atk_text_get_character_count(ATK_TEXT(node)), 0);
185 
186  fl_accessible_node_set_value(node, "Flutter!");
187 
188  EXPECT_EQ(atk_text_get_character_count(ATK_TEXT(node)), 8);
189 }
190 
191 // Tests AtkText::get_text.
193  g_autoptr(FlAccessibleNode) node =
194  fl_accessible_text_field_new(engine, 123, 1);
195 
196  g_autofree gchar* empty = atk_text_get_text(ATK_TEXT(node), 0, -1);
197  EXPECT_STREQ(empty, "");
198 
199  flutter::testing::MockSignalHandler text_inserted(node, "text-insert");
200  EXPECT_SIGNAL(text_inserted).Times(1);
201 
202  fl_accessible_node_set_value(node, "Flutter!");
203 
204  g_autofree gchar* flutter = atk_text_get_text(ATK_TEXT(node), 0, -1);
205  EXPECT_STREQ(flutter, "Flutter!");
206 
207  g_autofree gchar* tt = atk_text_get_text(ATK_TEXT(node), 3, 5);
208  EXPECT_STREQ(tt, "tt");
209 }
210 
211 // Tests AtkText::get_text with out-of-bounds offsets.
212 TEST_F(FlAccessibleTextFieldTest, GetTextBoundsChecking) {
213  g_autoptr(FlAccessibleNode) node =
214  fl_accessible_text_field_new(engine, 123, 1);
215 
216  fl_accessible_node_set_value(node, "Hello");
217 
218  // start beyond end of text
219  g_autofree gchar* beyond = atk_text_get_text(ATK_TEXT(node), 100, -1);
220  EXPECT_STREQ(beyond, "");
221 
222  // end beyond text length
223  g_autofree gchar* end_beyond = atk_text_get_text(ATK_TEXT(node), 2, 100);
224  EXPECT_STREQ(end_beyond, "llo");
225 
226  // both beyond text length
227  g_autofree gchar* both_beyond = atk_text_get_text(ATK_TEXT(node), 50, 100);
228  EXPECT_STREQ(both_beyond, "");
229 
230  // empty buffer
232  g_autofree gchar* empty_beyond = atk_text_get_text(ATK_TEXT(node), 5, 10);
233  EXPECT_STREQ(empty_beyond, "");
234 }
235 
236 // Tests AtkText::get_caret_offset.
238  g_autoptr(FlAccessibleNode) node =
239  fl_accessible_text_field_new(engine, 123, 1);
240 
241  EXPECT_EQ(atk_text_get_caret_offset(ATK_TEXT(node)), -1);
242 
244 
245  EXPECT_EQ(atk_text_get_caret_offset(ATK_TEXT(node)), 2);
246 }
247 
248 // Tests AtkText::set_caret_offset.
250  int base = -1;
251  int extent = -1;
252 
253  StartEngine();
254 
255  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
256  SendSemanticsAction,
257  ([&base, &extent](auto engine,
258  const FlutterSendSemanticsActionInfo* info) {
259  EXPECT_EQ(info->action, kFlutterSemanticsActionSetSelection);
261  decode_semantic_data(info->data, info->data_length);
264  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
265  return kSuccess;
266  }));
267 
268  g_autoptr(FlAccessibleNode) node =
269  fl_accessible_text_field_new(engine, 123, 1);
270 
271  EXPECT_TRUE(atk_text_set_caret_offset(ATK_TEXT(node), 3));
272  EXPECT_EQ(base, 3);
273  EXPECT_EQ(extent, 3);
274 }
275 
276 // Tests AtkText::get_n_selections.
278  g_autoptr(FlAccessibleNode) node =
279  fl_accessible_text_field_new(engine, 123, 1);
280 
281  EXPECT_EQ(atk_text_get_n_selections(ATK_TEXT(node)), 0);
282 
284 
285  EXPECT_EQ(atk_text_get_n_selections(ATK_TEXT(node)), 1);
286 }
287 
288 // Tests AtkText::get_selection.
290  g_autoptr(FlAccessibleNode) node =
291  fl_accessible_text_field_new(engine, 123, 1);
292 
293  EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 0, nullptr, nullptr),
294  nullptr);
295 
296  fl_accessible_node_set_value(node, "Flutter");
298 
299  gint start, end;
300  g_autofree gchar* selection =
301  atk_text_get_selection(ATK_TEXT(node), 0, &start, &end);
302  EXPECT_STREQ(selection, "utt");
303  EXPECT_EQ(start, 2);
304  EXPECT_EQ(end, 5);
305 
306  // reverse
308  g_autofree gchar* reverse =
309  atk_text_get_selection(ATK_TEXT(node), 0, &start, &end);
310  EXPECT_STREQ(reverse, "utt");
311  EXPECT_EQ(start, 2);
312  EXPECT_EQ(end, 5);
313 
314  // empty
316  EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 0, &start, &end), nullptr);
317 
318  // selection num != 0
319  EXPECT_EQ(atk_text_get_selection(ATK_TEXT(node), 1, &start, &end), nullptr);
320 }
321 
322 // Tests AtkText::add_selection.
324  int base = -1;
325  int extent = -1;
326 
327  StartEngine();
328 
329  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
330  SendSemanticsAction,
331  ([&base, &extent](auto engine,
332  const FlutterSendSemanticsActionInfo* info) {
333  EXPECT_EQ(info->action, kFlutterSemanticsActionSetSelection);
335  decode_semantic_data(info->data, info->data_length);
338  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
339  return kSuccess;
340  }));
341 
342  g_autoptr(FlAccessibleNode) node =
343  fl_accessible_text_field_new(engine, 123, 1);
344 
345  EXPECT_TRUE(atk_text_add_selection(ATK_TEXT(node), 2, 4));
346  EXPECT_EQ(base, 2);
347  EXPECT_EQ(extent, 4);
348 
350 
351  // already has selection
352  EXPECT_FALSE(atk_text_add_selection(ATK_TEXT(node), 6, 7));
353  EXPECT_EQ(base, 2);
354  EXPECT_EQ(extent, 4);
355 }
356 
357 // Tests AtkText::remove_selection.
359  int base = -1;
360  int extent = -1;
361 
362  StartEngine();
363 
364  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
365  SendSemanticsAction,
366  ([&base, &extent](auto engine,
367  const FlutterSendSemanticsActionInfo* info) {
368  EXPECT_EQ(info->action, kFlutterSemanticsActionSetSelection);
370  decode_semantic_data(info->data, info->data_length);
373  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
374  return kSuccess;
375  }));
376 
377  g_autoptr(FlAccessibleNode) node =
378  fl_accessible_text_field_new(engine, 123, 1);
379 
380  // no selection
381  EXPECT_FALSE(atk_text_remove_selection(ATK_TEXT(node), 0));
382  EXPECT_EQ(base, -1);
383  EXPECT_EQ(extent, -1);
384 
386 
387  // selection num != 0
388  EXPECT_FALSE(atk_text_remove_selection(ATK_TEXT(node), 1));
389  EXPECT_EQ(base, -1);
390  EXPECT_EQ(extent, -1);
391 
392  // ok, collapses selection
393  EXPECT_TRUE(atk_text_remove_selection(ATK_TEXT(node), 0));
394  EXPECT_EQ(base, 4);
395  EXPECT_EQ(extent, 4);
396 }
397 
398 // Tests AtkText::set_selection.
400  int base = -1;
401  int extent = -1;
402 
403  StartEngine();
404 
405  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
406  SendSemanticsAction,
407  ([&base, &extent](auto engine,
408  const FlutterSendSemanticsActionInfo* info) {
409  EXPECT_EQ(info->action, kFlutterSemanticsActionSetSelection);
411  decode_semantic_data(info->data, info->data_length);
414  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
415  return kSuccess;
416  }));
417 
418  g_autoptr(FlAccessibleNode) node =
419  fl_accessible_text_field_new(engine, 123, 1);
420 
421  // selection num != 0
422  EXPECT_FALSE(atk_text_set_selection(ATK_TEXT(node), 1, 2, 4));
423  EXPECT_EQ(base, -1);
424  EXPECT_EQ(extent, -1);
425 
426  EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(node), 0, 2, 4));
427  EXPECT_EQ(base, 2);
428  EXPECT_EQ(extent, 4);
429 
430  EXPECT_TRUE(atk_text_set_selection(ATK_TEXT(node), 0, 5, 1));
431  EXPECT_EQ(base, 5);
432  EXPECT_EQ(extent, 1);
433 }
434 
435 // Tests AtkEditableText::set_text_contents.
437  g_autofree gchar* text = nullptr;
438 
439  StartEngine();
440 
441  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
442  SendSemanticsAction,
443  ([&text](auto engine, const FlutterSendSemanticsActionInfo* info) {
444  EXPECT_EQ(info->action, kFlutterSemanticsActionSetText);
446  decode_semantic_data(info->data, info->data_length);
448  text = g_strdup(fl_value_get_string(value));
449  return kSuccess;
450  }));
451 
452  g_autoptr(FlAccessibleNode) node =
453  fl_accessible_text_field_new(engine, 123, 1);
454 
455  atk_editable_text_set_text_contents(ATK_EDITABLE_TEXT(node), "Flutter");
456  EXPECT_STREQ(text, "Flutter");
457 }
458 
459 // Tests AtkEditableText::insert/delete_text.
460 TEST_F(FlAccessibleTextFieldTest, InsertDeleteText) {
461  g_autofree gchar* text = nullptr;
462  int base = -1;
463  int extent = -1;
464 
465  StartEngine();
466 
467  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
468  SendSemanticsAction,
469  ([&text, &base, &extent](auto engine,
470  const FlutterSendSemanticsActionInfo* info) {
471  EXPECT_THAT(info->action,
472  ::testing::AnyOf(kFlutterSemanticsActionSetText,
473  kFlutterSemanticsActionSetSelection));
474  if (info->action == kFlutterSemanticsActionSetText) {
476  decode_semantic_data(info->data, info->data_length);
478  g_free(text);
479  text = g_strdup(fl_value_get_string(value));
480  } else {
482  decode_semantic_data(info->data, info->data_length);
485  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
486  }
487  return kSuccess;
488  }));
489 
490  g_autoptr(FlAccessibleNode) node =
491  fl_accessible_text_field_new(engine, 123, 1);
492  fl_accessible_node_set_value(node, "Fler");
493 
494  gint pos = 2;
495  atk_editable_text_insert_text(ATK_EDITABLE_TEXT(node), "utt", 3, &pos);
496  EXPECT_EQ(pos, 5);
497  EXPECT_STREQ(text, "Flutter");
498  EXPECT_EQ(base, pos);
499  EXPECT_EQ(extent, pos);
500 
501  atk_editable_text_delete_text(ATK_EDITABLE_TEXT(node), 2, 5);
502  EXPECT_STREQ(text, "Fler");
503  EXPECT_EQ(base, 2);
504  EXPECT_EQ(extent, 2);
505 }
506 
507 // Tests AtkEditableText::copy/cut/paste_text.
508 TEST_F(FlAccessibleTextFieldTest, CopyCutPasteText) {
509  int base = -1;
510  int extent = -1;
511  FlutterSemanticsAction act = kFlutterSemanticsActionCustomAction;
512 
513  StartEngine();
514 
515  fl_engine_get_embedder_api(engine)->SendSemanticsAction = MOCK_ENGINE_PROC(
516  SendSemanticsAction,
517  ([&act, &base, &extent](auto engine,
518  const FlutterSendSemanticsActionInfo* info) {
519  EXPECT_THAT(info->action,
520  ::testing::AnyOf(kFlutterSemanticsActionCut,
521  kFlutterSemanticsActionCopy,
522  kFlutterSemanticsActionPaste,
523  kFlutterSemanticsActionSetSelection));
524  act = info->action;
525  if (info->action == kFlutterSemanticsActionSetSelection) {
527  decode_semantic_data(info->data, info->data_length);
530  extent = fl_value_get_int(fl_value_lookup_string(value, "extent"));
531  }
532  return kSuccess;
533  }));
534 
535  g_autoptr(FlAccessibleNode) node =
536  fl_accessible_text_field_new(engine, 123, 1);
537 
538  atk_editable_text_copy_text(ATK_EDITABLE_TEXT(node), 2, 5);
539  EXPECT_EQ(base, 2);
540  EXPECT_EQ(extent, 5);
541  EXPECT_EQ(act, kFlutterSemanticsActionCopy);
542 
543  atk_editable_text_cut_text(ATK_EDITABLE_TEXT(node), 1, 4);
544  EXPECT_EQ(base, 1);
545  EXPECT_EQ(extent, 4);
546  EXPECT_EQ(act, kFlutterSemanticsActionCut);
547 
548  atk_editable_text_paste_text(ATK_EDITABLE_TEXT(node), 3);
549  EXPECT_EQ(base, 3);
550  EXPECT_EQ(extent, 3);
551  EXPECT_EQ(act, kFlutterSemanticsActionPaste);
552 }
553 
555  g_autoptr(FlAccessibleNode) node =
556  fl_accessible_text_field_new(engine, 123, 1);
557 
559  "Lorem ipsum.\nDolor sit amet. Praesent commodo?"
560  "\n\nPraesent et felis dui.");
561 
562  // |Lorem
563  gint start_offset = -1, end_offset = -1;
564  g_autofree gchar* lorem_char = atk_text_get_string_at_offset(
565  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset);
566  EXPECT_STREQ(lorem_char, "L");
567  EXPECT_EQ(start_offset, 0);
568  EXPECT_EQ(end_offset, 1);
569 
570  g_autofree gchar* lorem_word = atk_text_get_string_at_offset(
571  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset);
572  EXPECT_STREQ(lorem_word, "Lorem");
573  EXPECT_EQ(start_offset, 0);
574  EXPECT_EQ(end_offset, 5);
575 
576  g_autofree gchar* lorem_sentence = atk_text_get_string_at_offset(
577  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset,
578  &end_offset);
579  EXPECT_STREQ(lorem_sentence, "Lorem ipsum.");
580  EXPECT_EQ(start_offset, 0);
581  EXPECT_EQ(end_offset, 12);
582 
583  g_autofree gchar* lorem_line = atk_text_get_string_at_offset(
584  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_LINE, &start_offset, &end_offset);
585  EXPECT_STREQ(lorem_line, "Lorem ipsum.");
586  EXPECT_EQ(start_offset, 0);
587  EXPECT_EQ(end_offset, 12);
588 
589  g_autofree gchar* lorem_paragraph = atk_text_get_string_at_offset(
590  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_PARAGRAPH, &start_offset,
591  &end_offset);
592  EXPECT_STREQ(lorem_paragraph,
593  "Lorem ipsum.\nDolor sit amet. Praesent commodo?");
594  EXPECT_EQ(start_offset, 0);
595  EXPECT_EQ(end_offset, 46);
596 
597  // Pra|esent
598  g_autofree gchar* praesent_char = atk_text_get_string_at_offset(
599  ATK_TEXT(node), 32, ATK_TEXT_GRANULARITY_CHAR, &start_offset,
600  &end_offset);
601  EXPECT_STREQ(praesent_char, "e");
602  EXPECT_EQ(start_offset, 32);
603  EXPECT_EQ(end_offset, 33);
604 
605  g_autofree gchar* praesent_word = atk_text_get_string_at_offset(
606  ATK_TEXT(node), 32, ATK_TEXT_GRANULARITY_WORD, &start_offset,
607  &end_offset);
608  EXPECT_STREQ(praesent_word, "Praesent");
609  EXPECT_EQ(start_offset, 29);
610  EXPECT_EQ(end_offset, 37);
611 
612  g_autofree gchar* praesent_sentence = atk_text_get_string_at_offset(
613  ATK_TEXT(node), 32, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset,
614  &end_offset);
615  EXPECT_STREQ(praesent_sentence, "Praesent commodo?");
616  EXPECT_EQ(start_offset, 29);
617  EXPECT_EQ(end_offset, 46);
618 
619  g_autofree gchar* praesent_line = atk_text_get_string_at_offset(
620  ATK_TEXT(node), 32, ATK_TEXT_GRANULARITY_LINE, &start_offset,
621  &end_offset);
622  EXPECT_STREQ(praesent_line, "Dolor sit amet. Praesent commodo?");
623  EXPECT_EQ(start_offset, 13);
624  EXPECT_EQ(end_offset, 46);
625 
626  g_autofree gchar* praesent_paragraph = atk_text_get_string_at_offset(
627  ATK_TEXT(node), 32, ATK_TEXT_GRANULARITY_PARAGRAPH, &start_offset,
628  &end_offset);
629  EXPECT_STREQ(praesent_paragraph,
630  "Lorem ipsum.\nDolor sit amet. Praesent commodo?");
631  EXPECT_EQ(start_offset, 0);
632  EXPECT_EQ(end_offset, 46);
633 
634  // feli|s
635  g_autofree gchar* felis_char = atk_text_get_string_at_offset(
636  ATK_TEXT(node), 64, ATK_TEXT_GRANULARITY_CHAR, &start_offset,
637  &end_offset);
638  EXPECT_STREQ(felis_char, "s");
639  EXPECT_EQ(start_offset, 64);
640  EXPECT_EQ(end_offset, 65);
641 
642  g_autofree gchar* felis_word = atk_text_get_string_at_offset(
643  ATK_TEXT(node), 64, ATK_TEXT_GRANULARITY_WORD, &start_offset,
644  &end_offset);
645  EXPECT_STREQ(felis_word, "felis");
646  EXPECT_EQ(start_offset, 60);
647  EXPECT_EQ(end_offset, 65);
648 
649  g_autofree gchar* felis_sentence = atk_text_get_string_at_offset(
650  ATK_TEXT(node), 64, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset,
651  &end_offset);
652  EXPECT_STREQ(felis_sentence, "Praesent et felis dui.");
653  EXPECT_EQ(start_offset, 48);
654  EXPECT_EQ(end_offset, 70);
655 
656  g_autofree gchar* felis_line = atk_text_get_string_at_offset(
657  ATK_TEXT(node), 64, ATK_TEXT_GRANULARITY_LINE, &start_offset,
658  &end_offset);
659  EXPECT_STREQ(felis_line, "Praesent et felis dui.");
660  EXPECT_EQ(start_offset, 48);
661  EXPECT_EQ(end_offset, 70);
662 
663  g_autofree gchar* felis_paragraph = atk_text_get_string_at_offset(
664  ATK_TEXT(node), 64, ATK_TEXT_GRANULARITY_PARAGRAPH, &start_offset,
665  &end_offset);
666  EXPECT_STREQ(felis_paragraph, "\nPraesent et felis dui.");
667  EXPECT_EQ(start_offset, 47);
668  EXPECT_EQ(end_offset, 70);
669 }
670 
671 // Tests that get_string_at_offset handles offset beyond text length.
672 TEST_F(FlAccessibleTextFieldTest, TextBoundaryOffsetBeyondEnd) {
673  g_autoptr(FlAccessibleNode) node =
674  fl_accessible_text_field_new(engine, 123, 1);
675 
676  fl_accessible_node_set_value(node, "Hello");
677 
678  // Offset well beyond the text length should not crash and should return
679  // a valid result clamped to the text boundaries.
680  gint start_offset = -1, end_offset = -1;
681  g_autofree gchar* char_result = atk_text_get_string_at_offset(
682  ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_CHAR, &start_offset,
683  &end_offset);
684  EXPECT_NE(char_result, nullptr);
685  EXPECT_GE(start_offset, 0);
686  EXPECT_LE(end_offset, 5);
687 
688  g_autofree gchar* word_result = atk_text_get_string_at_offset(
689  ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_WORD, &start_offset,
690  &end_offset);
691  EXPECT_NE(word_result, nullptr);
692  EXPECT_GE(start_offset, 0);
693  EXPECT_LE(end_offset, 5);
694 
695  g_autofree gchar* sentence_result = atk_text_get_string_at_offset(
696  ATK_TEXT(node), 100, ATK_TEXT_GRANULARITY_SENTENCE, &start_offset,
697  &end_offset);
698  EXPECT_NE(sentence_result, nullptr);
699  EXPECT_GE(start_offset, 0);
700  EXPECT_LE(end_offset, 5);
701 }
702 
703 // Tests that get_string_at_offset handles offset at position zero.
704 TEST_F(FlAccessibleTextFieldTest, TextBoundaryOffsetAtStart) {
705  g_autoptr(FlAccessibleNode) node =
706  fl_accessible_text_field_new(engine, 123, 1);
707 
708  fl_accessible_node_set_value(node, "Hello");
709 
710  // Offset at zero should return the first character/word.
711  gint start_offset = -1, end_offset = -1;
712  g_autofree gchar* char_result = atk_text_get_string_at_offset(
713  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset);
714  EXPECT_NE(char_result, nullptr);
715  EXPECT_EQ(start_offset, 0);
716  EXPECT_EQ(end_offset, 1);
717  EXPECT_STREQ(char_result, "H");
718 
719  g_autofree gchar* word_result = atk_text_get_string_at_offset(
720  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset);
721  EXPECT_NE(word_result, nullptr);
722  EXPECT_EQ(start_offset, 0);
723  EXPECT_EQ(end_offset, 5);
724  EXPECT_STREQ(word_result, "Hello");
725 }
726 
727 // Tests that get_string_at_offset handles empty text.
728 TEST_F(FlAccessibleTextFieldTest, TextBoundaryEmptyText) {
729  g_autoptr(FlAccessibleNode) node =
730  fl_accessible_text_field_new(engine, 123, 1);
731 
732  // Empty text - should not crash.
733  gint start_offset = -1, end_offset = -1;
734  g_autofree gchar* char_result = atk_text_get_string_at_offset(
735  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_CHAR, &start_offset, &end_offset);
736  EXPECT_NE(char_result, nullptr);
737  EXPECT_EQ(start_offset, 0);
738  EXPECT_STREQ(char_result, "");
739 
740  g_autofree gchar* word_result = atk_text_get_string_at_offset(
741  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_WORD, &start_offset, &end_offset);
742  EXPECT_NE(word_result, nullptr);
743  EXPECT_EQ(start_offset, 0);
744  EXPECT_STREQ(word_result, "");
745 }
746 
747 // Tests that get_string_at_offset handles offset at exact text length.
748 TEST_F(FlAccessibleTextFieldTest, TextBoundaryOffsetAtEnd) {
749  g_autoptr(FlAccessibleNode) node =
750  fl_accessible_text_field_new(engine, 123, 1);
751 
752  fl_accessible_node_set_value(node, "Hello world");
753 
754  // Offset at exactly the character count (one past last char).
755  gint char_count = atk_text_get_character_count(ATK_TEXT(node));
756  EXPECT_EQ(char_count, 11);
757 
758  gint start_offset = -1, end_offset = -1;
759  g_autofree gchar* char_result = atk_text_get_string_at_offset(
760  ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_CHAR, &start_offset,
761  &end_offset);
762  EXPECT_NE(char_result, nullptr);
763  EXPECT_GE(start_offset, 0);
764  EXPECT_LE(end_offset, char_count);
765 
766  g_autofree gchar* word_result = atk_text_get_string_at_offset(
767  ATK_TEXT(node), char_count, ATK_TEXT_GRANULARITY_WORD, &start_offset,
768  &end_offset);
769  EXPECT_NE(word_result, nullptr);
770  EXPECT_GE(start_offset, 0);
771  EXPECT_LE(end_offset, char_count);
772 }
773 
774 // Tests that line and paragraph boundaries are computed in character offsets
775 // rather than byte offsets when the text contains multi-byte UTF-8 characters.
776 TEST_F(FlAccessibleTextFieldTest, TextBoundaryMultiByte) {
777  g_autoptr(FlAccessibleNode) node =
778  fl_accessible_text_field_new(engine, 123, 1);
779 
780  // "Café" is 4 characters but 5 bytes (é is 2 bytes).
781  // "Münch" is 5 characters but 6 bytes (ü is 2 bytes).
782  // Character offsets: C(0) a(1) f(2) é(3) \n(4) M(5) ü(6) n(7) c(8) h(9)
783  fl_accessible_node_set_value(node, "Café\nMünch");
784 
785  // The field has 10 characters.
786  EXPECT_EQ(atk_text_get_character_count(ATK_TEXT(node)), 10);
787 
788  // First line: "Café" at character offsets [0, 4).
789  gint start_offset = -1, end_offset = -1;
790  g_autofree gchar* line0 = atk_text_get_string_at_offset(
791  ATK_TEXT(node), 0, ATK_TEXT_GRANULARITY_LINE, &start_offset, &end_offset);
792  EXPECT_STREQ(line0, "Café");
793  EXPECT_EQ(start_offset, 0);
794  EXPECT_EQ(end_offset, 4);
795 
796  // Second line: "Münch" at character offsets [5, 10). Query at offset 6
797  // (the 'ü'), which sits in the middle of a multi-byte sequence in byte
798  // space - the buggy byte-based logic would return the wrong substring here.
799  g_autofree gchar* line1 = atk_text_get_string_at_offset(
800  ATK_TEXT(node), 6, ATK_TEXT_GRANULARITY_LINE, &start_offset, &end_offset);
801  EXPECT_STREQ(line1, "Münch");
802  EXPECT_EQ(start_offset, 5);
803  EXPECT_EQ(end_offset, 10);
804 
805  // Whole text is a single paragraph spanning both lines.
806  g_autofree gchar* paragraph = atk_text_get_string_at_offset(
807  ATK_TEXT(node), 6, ATK_TEXT_GRANULARITY_PARAGRAPH, &start_offset,
808  &end_offset);
809  EXPECT_STREQ(paragraph, "Café\nMünch");
810  EXPECT_EQ(start_offset, 0);
811  EXPECT_EQ(end_offset, 10);
812 }
813 
814 // NOLINTEND(clang-analyzer-core.StackAddressEscape)
g_autoptr(FlEngine) engine
void fl_accessible_node_set_text_selection(FlAccessibleNode *self, gint base, gint extent)
void fl_accessible_node_set_actions(FlAccessibleNode *self, FlutterSemanticsAction actions)
void fl_accessible_node_set_value(FlAccessibleNode *self, const gchar *value)
FlAccessibleNode * fl_accessible_text_field_new(FlEngine *engine, FlutterViewId view_id, int32_t id)
glong glong end
TEST_F(FlAccessibleTextFieldTest, SetValue)
static FlValue * decode_semantic_data(const uint8_t *data, size_t data_length)
FlutterEngineProcTable * fl_engine_get_embedder_api(FlEngine *self)
Definition: fl_engine.cc:940
G_MODULE_EXPORT FlValue * fl_message_codec_decode_message(FlMessageCodec *self, GBytes *message, GError **error)
uint8_t value
G_MODULE_EXPORT FlStandardMessageCodec * fl_standard_message_codec_new()
G_MODULE_EXPORT FlValue * fl_value_lookup_string(FlValue *self, const gchar *key)
Definition: fl_value.cc:811
G_MODULE_EXPORT int64_t fl_value_get_int(FlValue *self)
Definition: fl_value.cc:668
G_MODULE_EXPORT void fl_value_unref(FlValue *self)
Definition: fl_value.cc:400
G_MODULE_EXPORT FlValueType fl_value_get_type(FlValue *self)
Definition: fl_value.cc:466
G_MODULE_EXPORT FlValue * fl_value_new_bool(bool value)
Definition: fl_value.cc:255
G_MODULE_EXPORT bool fl_value_equal(FlValue *a, FlValue *b)
Definition: fl_value.cc:471
G_MODULE_EXPORT const gchar * fl_value_get_string(FlValue *self)
Definition: fl_value.cc:682
typedefG_BEGIN_DECLS struct _FlValue FlValue
Definition: fl_value.h:42
@ FL_VALUE_TYPE_STRING
Definition: fl_value.h:68
@ FL_VALUE_TYPE_MAP
Definition: fl_value.h:74