basalt-gtk 0.0.0-rc5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runLinux.js +29 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +362 -0
  5. package/native/gtk/ColorSchemeGtk.cpp +58 -0
  6. package/native/gtk/ComponentRegistryGtk.cpp +101 -0
  7. package/native/gtk/FontRegistryFontconfig.cpp +127 -0
  8. package/native/gtk/GtkAnimationChoreographer.cpp +73 -0
  9. package/native/gtk/GtkAnimationChoreographer.h +49 -0
  10. package/native/gtk/GtkDropTarget.cpp +269 -0
  11. package/native/gtk/GtkDropTarget.h +40 -0
  12. package/native/gtk/GtkFocus.cpp +296 -0
  13. package/native/gtk/GtkFocus.h +113 -0
  14. package/native/gtk/GtkImageLoader.cpp +128 -0
  15. package/native/gtk/GtkImageLoader.h +87 -0
  16. package/native/gtk/GtkKeyEvents.cpp +91 -0
  17. package/native/gtk/GtkKeyEvents.h +21 -0
  18. package/native/gtk/GtkMenuBar.cpp +45 -0
  19. package/native/gtk/GtkMountingManager.cpp +1027 -0
  20. package/native/gtk/GtkMountingManager.h +146 -0
  21. package/native/gtk/GtkRunLoopObserver.cpp +77 -0
  22. package/native/gtk/GtkRunLoopObserver.h +41 -0
  23. package/native/gtk/GtkScrollView.cpp +626 -0
  24. package/native/gtk/GtkScrollView.h +209 -0
  25. package/native/gtk/GtkSkiaContext.cpp +248 -0
  26. package/native/gtk/GtkSkiaContext.h +62 -0
  27. package/native/gtk/GtkSkiaModule.cpp +80 -0
  28. package/native/gtk/GtkSkiaModule.h +51 -0
  29. package/native/gtk/GtkSkiaPeer.cpp +285 -0
  30. package/native/gtk/GtkSkiaPeer.h +63 -0
  31. package/native/gtk/GtkTextInput.cpp +428 -0
  32. package/native/gtk/GtkTextInput.h +123 -0
  33. package/native/gtk/GtkTextPeer.cpp +425 -0
  34. package/native/gtk/GtkTextPeer.h +87 -0
  35. package/native/gtk/GtkTitleBar.cpp +273 -0
  36. package/native/gtk/GtkTitleBar.h +96 -0
  37. package/native/gtk/GtkTitleBarLayout.cpp +45 -0
  38. package/native/gtk/GtkTitleBarLayout.h +36 -0
  39. package/native/gtk/GtkTouchDispatcher.cpp +671 -0
  40. package/native/gtk/GtkTouchDispatcher.h +139 -0
  41. package/native/gtk/GtkWindowControl.cpp +223 -0
  42. package/native/gtk/GtkWindowModule.cpp +194 -0
  43. package/native/gtk/GtkWindowModule.h +62 -0
  44. package/native/gtk/PangoTextLayout.cpp +295 -0
  45. package/native/gtk/PangoTextLayout.h +54 -0
  46. package/native/gtk/PangoTextLayoutManager.cpp +140 -0
  47. package/native/gtk/PlatformServicesGtk.cpp +583 -0
  48. package/native/gtk/RnView.cpp +1407 -0
  49. package/native/gtk/RnView.h +339 -0
  50. package/native/gtk/demo_layout_gtk.cpp +57 -0
  51. package/native/gtk/main_gtk.cpp +1981 -0
  52. package/native/gtk/mount_harness_gtk.cpp +143 -0
  53. package/native/tests/TestMainGtk.cpp +25 -0
  54. package/native/tests/notification_stub.cpp +103 -0
  55. package/native/tests/test_accessibility.cpp +251 -0
  56. package/native/tests/test_focus.cpp +194 -0
  57. package/native/tests/test_hittest.cpp +242 -0
  58. package/native/tests/test_image.cpp +135 -0
  59. package/native/tests/test_key_events_gtk.cpp +112 -0
  60. package/native/tests/test_mounting.cpp +415 -0
  61. package/native/tests/test_scrollview.cpp +258 -0
  62. package/native/tests/test_skia_texture.cpp +120 -0
  63. package/native/tests/test_text.cpp +147 -0
  64. package/native/tests/test_textinput.cpp +598 -0
  65. package/native/tests/test_titlebar.cpp +137 -0
  66. package/native/tests/test_titlebar_drag.cpp +138 -0
  67. package/native/tests/test_version.cpp +49 -0
  68. package/native/tests/test_view.cpp +162 -0
  69. package/native/tests/test_viewprops.cpp +254 -0
  70. package/package.json +26 -0
  71. package/react-native.config.js +21 -0
@@ -0,0 +1,598 @@
1
+ // Tests for <TextInput>: the GtkText peer and the controlled-value loop.
2
+ //
3
+ // The interesting bug in a text field is not typing, it is the loop. React
4
+ // Native's <TextInput> is controlled: JavaScript owns the value, the widget
5
+ // reports every change, JavaScript re-renders and sends the value back down. If
6
+ // applying that prop looks like the user typing, the two chase each other
7
+ // forever; if the cursor is not preserved while applying it, the caret jumps
8
+ // home on every keystroke. Both are asserted below, because both were wrong
9
+ // first.
10
+ //
11
+ // Props are built through React Native's own RawProps parser rather than by
12
+ // assigning fields, because the fields that matter -- `traits.editable`,
13
+ // `traits.secureTextEntry` -- are const and only reachable that way. That also
14
+ // means these tests exercise the same parse the real thing does.
15
+
16
+ #include "TestHarness.h"
17
+ #include "EventRecorder.h"
18
+
19
+ #include "GtkMountingManager.h"
20
+ #include "GtkTextInput.h"
21
+
22
+ #include "GtkTextPeer.h"
23
+
24
+ #include <react/renderer/components/iostextinput/TextInputProps.h>
25
+ #include <react/renderer/components/textinput/TextInputEventEmitter.h>
26
+ #include <react/renderer/core/RawPropsParser.h>
27
+
28
+ #include <folly/dynamic.h>
29
+
30
+ using facebook::react::ContextContainer;
31
+ using facebook::react::EventEmitter;
32
+ using facebook::react::LayoutMetrics;
33
+ using facebook::react::PropsParserContext;
34
+ using facebook::react::RawProps;
35
+ using facebook::react::RawPropsParser;
36
+ using facebook::react::ShadowView;
37
+ using facebook::react::SurfaceId;
38
+ using facebook::react::Tag;
39
+ using facebook::react::TextInputProps;
40
+
41
+ namespace {
42
+
43
+ constexpr SurfaceId kSurfaceId = 1;
44
+
45
+ // One parser, prepared once: preparing walks every prop TextInputProps knows,
46
+ // which is not something to redo per test.
47
+ const RawPropsParser &textInputParser() {
48
+ static const RawPropsParser parser = []() {
49
+ RawPropsParser prepared;
50
+ prepared.prepare<TextInputProps>();
51
+ return prepared;
52
+ }();
53
+ return parser;
54
+ }
55
+
56
+ std::shared_ptr<const TextInputProps> makeProps(folly::dynamic raw) {
57
+ static const auto contextContainer = std::make_shared<const ContextContainer>();
58
+ PropsParserContext context{kSurfaceId, *contextContainer};
59
+
60
+ RawProps rawProps{std::move(raw)};
61
+ rawProps.parse(textInputParser());
62
+ return std::make_shared<const TextInputProps>(context, TextInputProps{}, rawProps);
63
+ }
64
+
65
+ ShadowView makeTextInput(Tag tag, folly::dynamic raw, float inset = 0.0F) {
66
+ LayoutMetrics metrics;
67
+ metrics.frame = {.origin = {.x = 0.0F, .y = 0.0F}, .size = {.width = 200.0F, .height = 40.0F}};
68
+ metrics.contentInsets = {.left = inset, .top = inset, .right = inset, .bottom = inset};
69
+
70
+ ShadowView view;
71
+ view.componentName = "TextInput";
72
+ view.surfaceId = kSurfaceId;
73
+ view.tag = tag;
74
+ view.props = makeProps(std::move(raw));
75
+ view.layoutMetrics = metrics;
76
+ return view;
77
+ }
78
+
79
+ // No event emitter, which is right for everything asserted on the widget side
80
+ // of the loop: the field's own state does not depend on anyone listening.
81
+ basalt::GtkTextInputManager makeManager() {
82
+ return basalt::GtkTextInputManager([](Tag) { return EventEmitter::Shared{}; });
83
+ }
84
+
85
+ // And one that is listened to. A real emitter over a real EventDispatcher --
86
+ // see tests/EventRecorder.h for why that is possible without a JavaScript
87
+ // runtime, which the comment here used to say it was not.
88
+ basalt::GtkTextInputManager makeRecordedManager(const basalt::testing::EventRecorder &recorder) {
89
+ auto emitter = recorder.emitter<facebook::react::TextInputEventEmitter>();
90
+ return basalt::GtkTextInputManager([emitter](Tag) { return emitter; });
91
+ }
92
+
93
+ // Through the seam, because a multiline peer is not a GtkEditable and asking
94
+ // it for one is a runtime critical rather than a compile error.
95
+ std::string textOf(RnView *view) {
96
+ GtkWidget *editable = rn_view_get_editable(view);
97
+ if (editable == nullptr) {
98
+ return {};
99
+ }
100
+ char *owned = rn_peer_get_text(editable);
101
+ std::string text = owned != nullptr ? owned : "";
102
+ g_free(owned);
103
+ return text;
104
+ }
105
+
106
+ // What a person typing does, as far as GtkText is concerned: an insertion the
107
+ // widget did not come from a prop.
108
+ void typeInto(RnView *view, const char *text, int position) {
109
+ int cursor = position;
110
+ gtk_editable_insert_text(
111
+ GTK_EDITABLE(rn_view_get_editable(view)), text, static_cast<int>(strlen(text)), &cursor);
112
+ }
113
+
114
+ } // namespace
115
+
116
+ TEST(textinput_mounts_a_real_editable) {
117
+ RnView *view = rn_view_new(10);
118
+ g_object_ref_sink(view);
119
+ auto manager = makeManager();
120
+
121
+ EXPECT(rn_view_get_editable(view) == nullptr);
122
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")));
123
+
124
+ // A GtkText, not a PangoLayout with a cursor drawn on it: input methods,
125
+ // selection, the clipboard and every Linux keybinding come with it.
126
+ EXPECT(rn_view_get_editable(view) != nullptr);
127
+ EXPECT(GTK_IS_TEXT(rn_view_get_editable(view)));
128
+ EXPECT_EQ(textOf(view), std::string("hello"));
129
+
130
+ g_object_unref(view);
131
+ }
132
+
133
+ TEST(textinput_applies_props_without_reporting_them_as_typing) {
134
+ RnView *view = rn_view_new(10);
135
+ g_object_ref_sink(view);
136
+ auto manager = makeManager();
137
+
138
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "a")));
139
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "ab")));
140
+ EXPECT_EQ(textOf(view), std::string("ab"));
141
+
142
+ // The event count is the observable half of the loop: applying a prop must
143
+ // not raise it, or the next command from JavaScript looks stale and is
144
+ // dropped. Nothing typed here, so it must still be zero -- which
145
+ // setTextAndSelection at count 0 proves by being accepted.
146
+ EXPECT(manager.dispatchCommand(
147
+ 10, "setTextAndSelection", folly::dynamic::array(0, "from js", 0, 0)));
148
+ EXPECT_EQ(textOf(view), std::string("from js"));
149
+
150
+ g_object_unref(view);
151
+ }
152
+
153
+ TEST(textinput_preserves_the_cursor_when_a_prop_arrives) {
154
+ RnView *view = rn_view_new(10);
155
+ g_object_ref_sink(view);
156
+ auto manager = makeManager();
157
+
158
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcd")));
159
+ gtk_editable_set_position(GTK_EDITABLE(rn_view_get_editable(view)), 2);
160
+
161
+ // A controlled field re-renders on every keystroke, so this happens
162
+ // constantly. Assigning the text resets GtkText's cursor to the start, which
163
+ // sends the caret home mid-word unless it is put back.
164
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abXcd")));
165
+ EXPECT_EQ(gtk_editable_get_position(GTK_EDITABLE(rn_view_get_editable(view))), 2);
166
+
167
+ g_object_unref(view);
168
+ }
169
+
170
+ TEST(textinput_drops_a_command_older_than_what_was_typed) {
171
+ RnView *view = rn_view_new(10);
172
+ g_object_ref_sink(view);
173
+ auto manager = makeManager();
174
+
175
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
176
+ typeInto(view, "fast", 0);
177
+
178
+ // The whole reason React Native counts events. JavaScript rendered against an
179
+ // empty field and is only now asking for that state; the user has since
180
+ // typed, so this must be ignored rather than undoing their work.
181
+ EXPECT(manager.dispatchCommand(
182
+ 10, "setTextAndSelection", folly::dynamic::array(0, "stale", 0, 0)));
183
+ EXPECT_EQ(textOf(view), std::string("fast"));
184
+
185
+ // A command that knows about the typing is applied.
186
+ EXPECT(manager.dispatchCommand(
187
+ 10, "setTextAndSelection", folly::dynamic::array(99, "current", 0, 0)));
188
+ EXPECT_EQ(textOf(view), std::string("current"));
189
+
190
+ g_object_unref(view);
191
+ }
192
+
193
+ // An uncontrolled field keeps what was typed into it.
194
+ //
195
+ // React Native re-sends `mostRecentEventCount` on every change, which is an
196
+ // Update mutation on its own, and `text` for an uncontrolled field is the empty
197
+ // string forever -- `TextInput.js` sends `text={value ?? defaultValue}` and an
198
+ // uncontrolled field with no default sends undefined. So a manager that applies
199
+ // the prop whenever it differs from the widget wipes the field on the user's
200
+ // second keystroke. Applying it only when the *prop* changes is what makes this
201
+ // work; found on Windows in phase 46, and this platform had the same bug.
202
+ TEST(textinput_keeps_what_was_typed_into_an_uncontrolled_field) {
203
+ RnView *view = rn_view_new(10);
204
+ g_object_ref_sink(view);
205
+ auto manager = makeManager();
206
+
207
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
208
+ typeInto(view, "abc", 0);
209
+ EXPECT_EQ(textOf(view), std::string("abc"));
210
+
211
+ // The re-render an uncontrolled field produces: no text, a bumped count.
212
+ manager.update(
213
+ view,
214
+ makeTextInput(10, folly::dynamic::object("text", "")("mostRecentEventCount", 1)));
215
+ EXPECT_EQ(textOf(view), std::string("abc"));
216
+
217
+ g_object_unref(view);
218
+ }
219
+
220
+ // The same staleness rule the commands already follow, applied to the prop. A
221
+ // value React rendered before the user's latest keystroke must not be applied,
222
+ // or a fast typist watches characters reorder themselves -- and it must not be
223
+ // *forgotten* either, or the value is lost once JavaScript catches up.
224
+ // The whole selection reaches JavaScript, not only the caret. Before this the
225
+ // metrics reported {cursor, 0} whatever was selected, so `onSelectionChange`
226
+ // could not tell a caret from a selected word.
227
+ TEST(textinput_reports_the_whole_selection_not_only_the_caret) {
228
+ RnView *view = rn_view_new(10);
229
+ g_object_ref_sink(view);
230
+ auto manager = makeManager();
231
+
232
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
233
+ GtkWidget *editable = rn_view_get_editable(view);
234
+ rn_peer_select_region(editable, 1, 4);
235
+
236
+ int start = 0;
237
+ int end = 0;
238
+ EXPECT(rn_peer_get_selection_bounds(editable, &start, &end));
239
+ EXPECT_EQ(start, 1);
240
+ EXPECT_EQ(end, 4);
241
+
242
+ g_object_unref(view);
243
+ }
244
+
245
+ // The `selection` prop is controlled the way `text` is, and under the same
246
+ // staleness rule.
247
+ TEST(textinput_applies_a_selection_prop) {
248
+ RnView *view = rn_view_new(10);
249
+ g_object_ref_sink(view);
250
+ auto manager = makeManager();
251
+
252
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
253
+ manager.update(view,
254
+ makeTextInput(10,
255
+ folly::dynamic::object("text", "abcdef")(
256
+ "selection", folly::dynamic::object("start", 2)("end", 5))));
257
+
258
+ GtkWidget *editable = rn_view_get_editable(view);
259
+ int start = 0;
260
+ int end = 0;
261
+ EXPECT(rn_peer_get_selection_bounds(editable, &start, &end));
262
+ EXPECT_EQ(start, 2);
263
+ EXPECT_EQ(end, 5);
264
+
265
+ g_object_unref(view);
266
+ }
267
+
268
+ // A selection from JavaScript that predates the last keystroke is dropped, for
269
+ // the same reason a stale `text` is: it would drag the caret back to where
270
+ // JavaScript thought it was, mid-word.
271
+ TEST(textinput_drops_a_selection_prop_older_than_what_was_typed) {
272
+ RnView *view = rn_view_new(10);
273
+ g_object_ref_sink(view);
274
+ auto manager = makeManager();
275
+
276
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "abcdef")));
277
+ GtkWidget *editable = rn_view_get_editable(view);
278
+
279
+ // Type, which raises the event count past what the next props carry. The
280
+ // position is in/out and must be a real variable: GtkEditable writes the
281
+ // caret's new home back through it.
282
+ int position = 6;
283
+ rn_peer_insert_text(editable, "g", 1, &position);
284
+
285
+ rn_peer_select_region(editable, 0, 0);
286
+ manager.update(view,
287
+ makeTextInput(10,
288
+ folly::dynamic::object("text", "abcdef")(
289
+ "selection", folly::dynamic::object("start", 2)("end", 5))));
290
+
291
+ int start = 0;
292
+ int end = 0;
293
+ rn_peer_get_selection_bounds(editable, &start, &end);
294
+ EXPECT_EQ(start, 0);
295
+ EXPECT_EQ(end, 0);
296
+
297
+ g_object_unref(view);
298
+ }
299
+
300
+ TEST(textinput_drops_a_text_prop_older_than_what_was_typed) {
301
+ RnView *view = rn_view_new(10);
302
+ g_object_ref_sink(view);
303
+ auto manager = makeManager();
304
+
305
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
306
+ typeInto(view, "fast", 0);
307
+
308
+ manager.update(
309
+ view,
310
+ makeTextInput(10, folly::dynamic::object("text", "F")("mostRecentEventCount", 0)));
311
+ EXPECT_EQ(textOf(view), std::string("fast"));
312
+
313
+ manager.update(
314
+ view,
315
+ makeTextInput(10, folly::dynamic::object("text", "F")("mostRecentEventCount", 1)));
316
+ EXPECT_EQ(textOf(view), std::string("F"));
317
+
318
+ g_object_unref(view);
319
+ }
320
+
321
+ TEST(textinput_honours_editable_and_secure_entry) {
322
+ RnView *view = rn_view_new(10);
323
+ g_object_ref_sink(view);
324
+ auto manager = makeManager();
325
+
326
+ manager.update(view,
327
+ makeTextInput(10,
328
+ folly::dynamic::object("text", "secret")("editable", false)(
329
+ "secureTextEntry", true)("placeholder", "type here")));
330
+
331
+ GtkWidget *editable = rn_view_get_editable(view);
332
+ EXPECT(gtk_editable_get_editable(GTK_EDITABLE(editable)) == FALSE);
333
+ EXPECT(gtk_text_get_visibility(GTK_TEXT(editable)) == FALSE);
334
+ EXPECT_EQ(std::string(gtk_text_get_placeholder_text(GTK_TEXT(editable))), std::string("type here"));
335
+
336
+ // editable is a prop, not a permanent trait; turning it back on must work.
337
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "secret")));
338
+ EXPECT(gtk_editable_get_editable(GTK_EDITABLE(editable)) == TRUE);
339
+ EXPECT(gtk_text_get_visibility(GTK_TEXT(editable)) == TRUE);
340
+
341
+ g_object_unref(view);
342
+ }
343
+
344
+ TEST(textinput_allocates_its_peer_inside_the_content_inset) {
345
+ RnView *view = rn_view_new(10);
346
+ g_object_ref_sink(view);
347
+ auto manager = makeManager();
348
+
349
+ // Yoga resolves border and padding into the content inset. A GtkText knows
350
+ // nothing about either, so without this the text sits flush against the
351
+ // field's own border and paddingHorizontal does nothing.
352
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "x"), 12.0F));
353
+ rn_view_set_frame(view, 0.0F, 0.0F, 200.0F, 40.0F);
354
+ gtk_widget_allocate(GTK_WIDGET(view), 200, 40, -1, nullptr);
355
+
356
+ GtkWidget *peer = GTK_WIDGET(rn_view_get_editable(view));
357
+ EXPECT_EQ(gtk_widget_get_width(peer), 176);
358
+ EXPECT_EQ(gtk_widget_get_height(peer), 16);
359
+
360
+ graphene_point_t origin;
361
+ const graphene_point_t zero = {0.0F, 0.0F};
362
+ EXPECT(gtk_widget_compute_point(peer, GTK_WIDGET(view), &zero, &origin));
363
+ EXPECT_NEAR(origin.x, 12.0, 0.5);
364
+ EXPECT_NEAR(origin.y, 12.0, 0.5);
365
+
366
+ g_object_unref(view);
367
+ }
368
+
369
+ TEST(textinput_reports_its_value_in_the_widget_tree) {
370
+ RnView *view = rn_view_new(10);
371
+ g_object_ref_sink(view);
372
+ auto manager = makeManager();
373
+
374
+ // The end-to-end suite reads this tree, and a field's text lives in its peer
375
+ // rather than in a PangoLayout, so it would otherwise be invisible there.
376
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "in the tree")));
377
+ char *tree = rn_view_describe_tree(view);
378
+ EXPECT(std::string(tree).find("editable=\"in the tree\"") != std::string::npos);
379
+ g_free(tree);
380
+
381
+ g_object_unref(view);
382
+ }
383
+
384
+ // --- multiline ---------------------------------------------------------------
385
+ //
386
+ // A multiline field is a different widget, not a property: GtkText is a
387
+ // GtkEditable and GtkTextView is not. These pin that the seam in GtkTextPeer.h
388
+ // really does hide that, because every one of the behaviours above goes
389
+ // through it.
390
+
391
+ TEST(textinput_multiline_builds_a_text_view) {
392
+ RnView *view = rn_view_new(10);
393
+ g_object_ref_sink(view);
394
+ auto manager = makeManager();
395
+
396
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")("multiline", true)));
397
+
398
+ GtkWidget *peer = rn_view_get_editable(view);
399
+ EXPECT(peer != nullptr);
400
+ EXPECT(rn_peer_is_multiline(peer));
401
+ EXPECT(GTK_IS_TEXT_VIEW(peer));
402
+
403
+ // And a plain field is still a GtkText, which is the half that must not have
404
+ // moved.
405
+ RnView *single = rn_view_new(11);
406
+ g_object_ref_sink(single);
407
+ manager.update(single, makeTextInput(11, folly::dynamic::object("text", "hello")));
408
+ EXPECT(!rn_peer_is_multiline(rn_view_get_editable(single)));
409
+
410
+ g_object_unref(single);
411
+ g_object_unref(view);
412
+ }
413
+
414
+ TEST(textinput_multiline_text_round_trips) {
415
+ RnView *view = rn_view_new(10);
416
+ g_object_ref_sink(view);
417
+ auto manager = makeManager();
418
+
419
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "one")("multiline", true)));
420
+ EXPECT_EQ(textOf(view), std::string("one"));
421
+
422
+ // Newlines are the point of a multiline field, and are what a GtkText would
423
+ // have refused to hold.
424
+ manager.update(view,
425
+ makeTextInput(10, folly::dynamic::object("text", "one\ntwo")("multiline", true)));
426
+ EXPECT_EQ(textOf(view), std::string("one\ntwo"));
427
+
428
+ g_object_unref(view);
429
+ }
430
+
431
+ TEST(textinput_multiline_selection_and_caret_work) {
432
+ RnView *view = rn_view_new(10);
433
+ g_object_ref_sink(view);
434
+ auto manager = makeManager();
435
+
436
+ manager.update(view,
437
+ makeTextInput(10, folly::dynamic::object("text", "abcdef")("multiline", true)));
438
+ GtkWidget *peer = rn_view_get_editable(view);
439
+
440
+ rn_peer_select_region(peer, 1, 4);
441
+ int start = 0;
442
+ int end = 0;
443
+ EXPECT(rn_peer_get_selection_bounds(peer, &start, &end));
444
+ EXPECT_EQ(start, 1);
445
+ EXPECT_EQ(end, 4);
446
+
447
+ rn_peer_set_position(peer, 2);
448
+ EXPECT_EQ(rn_peer_get_position(peer), 2);
449
+
450
+ // Offsets are characters, not bytes: the seam says so and a buffer counts
451
+ // both, so a non-ASCII string is where that promise is kept or broken.
452
+ manager.update(view,
453
+ makeTextInput(10, folly::dynamic::object("text", "a\u00e9c")("multiline", true)));
454
+ rn_peer_set_position(peer, 2);
455
+ EXPECT_EQ(rn_peer_get_position(peer), 2);
456
+
457
+ g_object_unref(view);
458
+ }
459
+
460
+ TEST(textinput_switching_multiline_rebuilds_the_peer) {
461
+ RnView *view = rn_view_new(10);
462
+ g_object_ref_sink(view);
463
+ auto manager = makeManager();
464
+
465
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")));
466
+ EXPECT(!rn_peer_is_multiline(rn_view_get_editable(view)));
467
+
468
+ // Not a property that can be flipped, so the widget is replaced -- and the
469
+ // text has to survive that, because React did not ask for it to be cleared.
470
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "hello")("multiline", true)));
471
+ EXPECT(rn_peer_is_multiline(rn_view_get_editable(view)));
472
+ EXPECT_EQ(textOf(view), std::string("hello"));
473
+
474
+ g_object_unref(view);
475
+ }
476
+
477
+ // maxLength, on the peer that has no property for it. GtkText enforces its own;
478
+ // a buffer refuses the insertion that would cross the limit instead.
479
+ TEST(textinput_multiline_refuses_text_past_max_length) {
480
+ RnView *view = rn_view_new(10);
481
+ g_object_ref_sink(view);
482
+ auto manager = makeManager();
483
+
484
+ manager.update(view,
485
+ makeTextInput(10, folly::dynamic::object("text", "")("multiline", true)(
486
+ "maxLength", 5)));
487
+ GtkWidget *peer = rn_view_get_editable(view);
488
+
489
+ int at = 0;
490
+ rn_peer_insert_text(peer, "abcd", 4, &at);
491
+ EXPECT_EQ(textOf(view), std::string("abcd"));
492
+
493
+ // Refused whole rather than truncated, which is what GtkText does with its
494
+ // own limit -- a paste that would overflow leaves what was there.
495
+ at = 4;
496
+ rn_peer_insert_text(peer, "efgh", 4, &at);
497
+ EXPECT_EQ(textOf(view), std::string("abcd"));
498
+
499
+ // And one that fits still lands.
500
+ at = 4;
501
+ rn_peer_insert_text(peer, "e", 1, &at);
502
+ EXPECT_EQ(textOf(view), std::string("abcde"));
503
+
504
+ g_object_unref(view);
505
+ }
506
+
507
+ // A placeholder on the peer that has no property for it. Drawn rather than set,
508
+ // so what a test can reach is the string the peer was given -- the drawing
509
+ // itself is in the screenshot that went with phase 57.
510
+ TEST(textinput_multiline_takes_a_placeholder) {
511
+ RnView *view = rn_view_new(10);
512
+ g_object_ref_sink(view);
513
+ auto manager = makeManager();
514
+
515
+ manager.update(view,
516
+ makeTextInput(10, folly::dynamic::object("text", "")("multiline", true)(
517
+ "placeholder", "type here")));
518
+ GtkWidget *peer = rn_view_get_editable(view);
519
+ EXPECT(rn_peer_is_multiline(peer));
520
+ const char *got = rn_peer_get_placeholder(peer);
521
+ EXPECT_EQ(std::string(got != nullptr ? got : ""), std::string("type here"));
522
+
523
+ // And the single-line peer still takes its own, which is a property rather
524
+ // than something drawn.
525
+ RnView *single = rn_view_new(11);
526
+ g_object_ref_sink(single);
527
+ manager.update(single,
528
+ makeTextInput(11, folly::dynamic::object("text", "")("placeholder", "type here")));
529
+ const char *single_got = rn_peer_get_placeholder(rn_view_get_editable(single));
530
+ EXPECT_EQ(std::string(single_got != nullptr ? single_got : ""), std::string("type here"));
531
+
532
+ g_object_unref(single);
533
+ g_object_unref(view);
534
+ }
535
+
536
+ TEST(textinput_drops_its_peer_when_the_field_is_removed) {
537
+ RnView *view = rn_view_new(10);
538
+ g_object_ref_sink(view);
539
+ auto manager = makeManager();
540
+
541
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "x")));
542
+ manager.remove(10);
543
+
544
+ // The signal handlers hold a pointer into the manager's own map, so the
545
+ // widget has to go with the entry or a later `changed` writes through a
546
+ // dangling pointer.
547
+ EXPECT(rn_view_get_editable(view) == nullptr);
548
+ EXPECT(!manager.dispatchCommand(10, "focus", folly::dynamic::array()));
549
+
550
+ g_object_unref(view);
551
+ }
552
+
553
+ // --- What React actually hears ---------------------------------------------
554
+ //
555
+ // Nothing below the end-to-end suite could see an event until
556
+ // tests/EventRecorder.h, so every assertion above is about the widget and
557
+ // none is about the loop. These two are about the loop.
558
+
559
+ TEST(textinput_change_reaches_the_emitter) {
560
+ basalt::testing::EventRecorder recorder;
561
+ RnView *view = rn_view_new(10);
562
+ g_object_ref_sink(view);
563
+ auto manager = makeRecordedManager(recorder);
564
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
565
+
566
+ // Mounting a field is not the user doing anything, so React hears nothing
567
+ // yet. Worth pinning: a recorder wired up wrong would be just as quiet.
568
+ EXPECT(recorder.seen().empty());
569
+
570
+ typeInto(view, "a", 0);
571
+
572
+ const auto seen = recorder.seen();
573
+ EXPECT_EQ(seen.size(), 1U);
574
+ EXPECT_EQ(seen.empty() ? std::string{} : seen[0], std::string("topChange"));
575
+
576
+ g_object_unref(view);
577
+ }
578
+
579
+ // A value pushed down as a prop is React talking to the field, not the field
580
+ // talking back -- so it must not come back up as a change. That is the
581
+ // controlled-value loop the top of this file describes, and the half of it
582
+ // that could not be checked until an event could be observed at all: the
583
+ // tests above could see that the text was right, not that the field stayed
584
+ // quiet about it.
585
+ TEST(textinput_a_prop_does_not_report_itself_as_a_change) {
586
+ basalt::testing::EventRecorder recorder;
587
+ RnView *view = rn_view_new(10);
588
+ g_object_ref_sink(view);
589
+ auto manager = makeRecordedManager(recorder);
590
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "")));
591
+
592
+ manager.update(view, makeTextInput(10, folly::dynamic::object("text", "from React")));
593
+
594
+ EXPECT_EQ(textOf(view), std::string("from React"));
595
+ EXPECT(recorder.seen().empty());
596
+
597
+ g_object_unref(view);
598
+ }