basalt-win32 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/cli/runWindows.js +35 -0
  3. package/cli/shared.js +75 -0
  4. package/native/CMakeLists.txt +398 -0
  5. package/native/tests/test_win32_accessibility.cpp +271 -0
  6. package/native/tests/test_win32_clipboard.cpp +121 -0
  7. package/native/tests/test_win32_focus.cpp +195 -0
  8. package/native/tests/test_win32_hittest.cpp +410 -0
  9. package/native/tests/test_win32_image.cpp +239 -0
  10. package/native/tests/test_win32_imageloader.cpp +208 -0
  11. package/native/tests/test_win32_input.cpp +361 -0
  12. package/native/tests/test_win32_key_events.cpp +84 -0
  13. package/native/tests/test_win32_mounting.cpp +410 -0
  14. package/native/tests/test_win32_paint.cpp +505 -0
  15. package/native/tests/test_win32_scrollview.cpp +385 -0
  16. package/native/tests/test_win32_text.cpp +258 -0
  17. package/native/tests/test_win32_textinput.cpp +609 -0
  18. package/native/tests/test_win32_titlebar.cpp +111 -0
  19. package/native/tests/test_win32_titlebar_actions.cpp +62 -0
  20. package/native/tests/test_win32_view.cpp +222 -0
  21. package/native/win32/ColorSchemeWin32.cpp +56 -0
  22. package/native/win32/ComponentRegistryWin32.cpp +124 -0
  23. package/native/win32/DirectWriteLayout.cpp +116 -0
  24. package/native/win32/DirectWriteLayout.h +36 -0
  25. package/native/win32/DirectWriteLayoutManager.cpp +124 -0
  26. package/native/win32/FontRegistryDirectWrite.cpp +117 -0
  27. package/native/win32/PlatformServicesWin32.cpp +643 -0
  28. package/native/win32/RnWin32Accessible.cpp +258 -0
  29. package/native/win32/RnWin32Accessible.h +91 -0
  30. package/native/win32/RnWin32Image.cpp +265 -0
  31. package/native/win32/RnWin32Image.h +93 -0
  32. package/native/win32/RnWin32TextLayout.cpp +310 -0
  33. package/native/win32/RnWin32TextLayout.h +160 -0
  34. package/native/win32/RnWin32View.cpp +1281 -0
  35. package/native/win32/RnWin32View.h +506 -0
  36. package/native/win32/Win32AnimationChoreographer.cpp +76 -0
  37. package/native/win32/Win32AnimationChoreographer.h +61 -0
  38. package/native/win32/Win32Clip.h +173 -0
  39. package/native/win32/Win32DropTarget.cpp +567 -0
  40. package/native/win32/Win32DropTarget.h +42 -0
  41. package/native/win32/Win32Focus.cpp +262 -0
  42. package/native/win32/Win32Focus.h +109 -0
  43. package/native/win32/Win32ImageLoader.cpp +167 -0
  44. package/native/win32/Win32ImageLoader.h +104 -0
  45. package/native/win32/Win32KeyEvents.cpp +126 -0
  46. package/native/win32/Win32KeyEvents.h +20 -0
  47. package/native/win32/Win32MenuBar.cpp +352 -0
  48. package/native/win32/Win32MenuBar.h +16 -0
  49. package/native/win32/Win32MountingManager.cpp +727 -0
  50. package/native/win32/Win32MountingManager.h +203 -0
  51. package/native/win32/Win32Packaging.cpp +161 -0
  52. package/native/win32/Win32Packaging.h +56 -0
  53. package/native/win32/Win32RunLoopObserver.cpp +48 -0
  54. package/native/win32/Win32RunLoopObserver.h +42 -0
  55. package/native/win32/Win32ScrollView.cpp +515 -0
  56. package/native/win32/Win32ScrollView.h +208 -0
  57. package/native/win32/Win32SkiaContext.cpp +221 -0
  58. package/native/win32/Win32SkiaContext.h +53 -0
  59. package/native/win32/Win32SkiaModule.cpp +80 -0
  60. package/native/win32/Win32SkiaModule.h +50 -0
  61. package/native/win32/Win32SkiaPeer.cpp +358 -0
  62. package/native/win32/Win32SkiaPeer.h +66 -0
  63. package/native/win32/Win32SkiaUnicode.cpp +42 -0
  64. package/native/win32/Win32Snapshot.cpp +201 -0
  65. package/native/win32/Win32Snapshot.h +66 -0
  66. package/native/win32/Win32Strings.h +54 -0
  67. package/native/win32/Win32TextInput.cpp +811 -0
  68. package/native/win32/Win32TextInput.h +232 -0
  69. package/native/win32/Win32TitleBar.cpp +562 -0
  70. package/native/win32/Win32TitleBar.h +146 -0
  71. package/native/win32/Win32TitleBarLayout.cpp +46 -0
  72. package/native/win32/Win32TitleBarLayout.h +58 -0
  73. package/native/win32/Win32TouchDispatcher.cpp +443 -0
  74. package/native/win32/Win32TouchDispatcher.h +153 -0
  75. package/native/win32/Win32UiThread.h +27 -0
  76. package/native/win32/Win32WindowControl.cpp +329 -0
  77. package/native/win32/Win32WindowModule.cpp +209 -0
  78. package/native/win32/Win32WindowModule.h +72 -0
  79. package/native/win32/demo_layout_win32.cpp +232 -0
  80. package/native/win32/main_win32.cpp +2481 -0
  81. package/native/win32/mount_harness_win32.cpp +316 -0
  82. package/package.json +26 -0
  83. package/react-native.config.js +22 -0
@@ -0,0 +1,609 @@
1
+ // Tests for <TextInput> on Windows: the peer, the controlled-value loop, the
2
+ // placement and the commands.
3
+ //
4
+ // These make a real window and real EDIT controls. That is unusual for this
5
+ // suite -- every other Windows test runs without one, because Direct2D renders
6
+ // offscreen -- and it is unavoidable here: a text field's peer *is* a window,
7
+ // so a test that mocks it away tests nothing that could break. The window is
8
+ // never shown, which is what keeps this runnable on a build agent.
9
+ //
10
+ // What still cannot be asserted from here is the events. An EventEmitter built
11
+ // by hand has no EventDispatcher, so `onChange` and `onFocus` go nowhere; the
12
+ // end-to-end proof is `js/input.js` under BASALT_TEST_TAP and BASALT_TEST_TYPE,
13
+ // What is left is everything the
14
+ // control itself can be asked: its text, its selection, its read-only and
15
+ // password state, where it is, and whether the loop that writes it is broken
16
+ // in the right place.
17
+
18
+ #include "TestHarness.h"
19
+
20
+ #include "Win32MountingManager.h"
21
+ #include "Win32Strings.h"
22
+ #include "Win32TextInput.h"
23
+
24
+ #include <react/renderer/components/iostextinput/TextInputProps.h>
25
+ #include <react/renderer/components/view/ViewProps.h>
26
+ #include <react/renderer/core/PropsParserContext.h>
27
+ #include <react/renderer/core/RawProps.h>
28
+ #include <react/renderer/core/RawPropsParser.h>
29
+
30
+ #include <memory>
31
+ #include <string>
32
+
33
+ using basalt::Win32MountingManager;
34
+ using basalt::win32::narrow;
35
+ using basalt::win32::RnWin32View;
36
+ using facebook::react::ContextContainer;
37
+ using facebook::react::LayoutMetrics;
38
+ using facebook::react::MountingTransaction;
39
+ using facebook::react::PropsParserContext;
40
+ using facebook::react::RawProps;
41
+ using facebook::react::RawPropsParser;
42
+ using facebook::react::ShadowView;
43
+ using facebook::react::ShadowViewMutation;
44
+ using facebook::react::ShadowViewMutationList;
45
+ using facebook::react::SurfaceId;
46
+ using facebook::react::Tag;
47
+ using facebook::react::TextInputProps;
48
+ using facebook::react::TransactionTelemetry;
49
+ using facebook::react::ViewProps;
50
+
51
+ namespace {
52
+
53
+ constexpr SurfaceId kSurfaceId = 1;
54
+
55
+ // The manager the test window forwards notifications to. A window procedure
56
+ // takes no context this suite can give it, and the alternative -- a window per
57
+ // manager -- would register a class per test.
58
+ Win32MountingManager *gCurrentManager = nullptr;
59
+
60
+ // The same forwarding the real host does, and it has to be here or nothing
61
+ // works: an EDIT reports EN_CHANGE to its *parent*, so a test window that
62
+ // answered with DefWindowProc alone would leave `eventCount` at zero and every
63
+ // staleness check trivially satisfied. That is the shape of a test that passes
64
+ // while the thing it describes is broken.
65
+ LRESULT CALLBACK testHostProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
66
+ if (message == WM_COMMAND && gCurrentManager != nullptr &&
67
+ gCurrentManager->handleControlCommand(wparam, lparam)) {
68
+ return 0;
69
+ }
70
+ return DefWindowProc(hwnd, message, wparam, lparam);
71
+ }
72
+
73
+ // One hidden window for the whole suite, because registering a class and
74
+ // creating a window per test is slower than the tests are and buys nothing:
75
+ // every peer is destroyed with its entry, so nothing leaks between them.
76
+ HWND testHostWindow() {
77
+ static HWND window = [] {
78
+ WNDCLASSEX windowClass{};
79
+ windowClass.cbSize = sizeof(windowClass);
80
+ windowClass.lpfnWndProc = testHostProc;
81
+ windowClass.hInstance = GetModuleHandle(nullptr);
82
+ windowClass.lpszClassName = L"BasaltTestHost";
83
+ RegisterClassEx(&windowClass);
84
+ // WS_POPUP and never shown: a control still lays out, takes text and
85
+ // reports its rectangle inside a window nobody can see.
86
+ return CreateWindowEx(0,
87
+ L"BasaltTestHost",
88
+ L"",
89
+ WS_POPUP | WS_CLIPCHILDREN,
90
+ 0,
91
+ 0,
92
+ 800,
93
+ 600,
94
+ nullptr,
95
+ nullptr,
96
+ GetModuleHandle(nullptr),
97
+ nullptr);
98
+ }();
99
+ return window;
100
+ }
101
+
102
+ ShadowView makeView(Tag tag, float x, float y, float width, float height) {
103
+ LayoutMetrics metrics;
104
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
105
+
106
+ ShadowView view;
107
+ view.componentName = "View";
108
+ view.surfaceId = kSurfaceId;
109
+ view.tag = tag;
110
+ view.props = std::make_shared<ViewProps>();
111
+ view.layoutMetrics = metrics;
112
+ return view;
113
+ }
114
+
115
+ // One parser, prepared once: preparing walks every prop TextInputProps knows,
116
+ // which is not something to redo per test. RawProps has to be parsed before it
117
+ // can be read -- constructing one and handing it straight to a Props
118
+ // constructor trips an assertion inside RawProps rather than producing empty
119
+ // props, which is a better failure than it sounds and is what both other
120
+ // suites' comments say too.
121
+ //
122
+ // Through the parser rather than by assignment because TextInputProps' `traits`
123
+ // is const and only reachable this way.
124
+ const RawPropsParser &textInputParser() {
125
+ static const RawPropsParser parser = []() {
126
+ RawPropsParser prepared;
127
+ prepared.prepare<TextInputProps>();
128
+ return prepared;
129
+ }();
130
+ return parser;
131
+ }
132
+
133
+ struct FieldOptions {
134
+ std::string text{};
135
+ std::string placeholder{};
136
+ int mostRecentEventCount{0};
137
+ bool editable{true};
138
+ bool secure{false};
139
+ int maxLength{0};
140
+ float padding{0};
141
+ };
142
+
143
+ ShadowView makeField(Tag tag,
144
+ float x,
145
+ float y,
146
+ float width,
147
+ float height,
148
+ const FieldOptions &options = {}) {
149
+ static const auto contextContainer = std::make_shared<const ContextContainer>();
150
+ PropsParserContext context{kSurfaceId, *contextContainer};
151
+
152
+ folly::dynamic raw = folly::dynamic::object("text", options.text)(
153
+ "mostRecentEventCount", options.mostRecentEventCount)("editable", options.editable)(
154
+ "secureTextEntry", options.secure);
155
+ if (!options.placeholder.empty()) {
156
+ raw["placeholder"] = options.placeholder;
157
+ }
158
+ if (options.maxLength > 0) {
159
+ raw["maxLength"] = options.maxLength;
160
+ }
161
+
162
+ RawProps rawProps{std::move(raw)};
163
+ rawProps.parse(textInputParser());
164
+ auto parsed = std::make_shared<const TextInputProps>(context, TextInputProps{}, rawProps);
165
+
166
+ LayoutMetrics metrics;
167
+ metrics.frame = {.origin = {.x = x, .y = y}, .size = {.width = width, .height = height}};
168
+ metrics.contentInsets = {.left = options.padding,
169
+ .top = options.padding,
170
+ .right = options.padding,
171
+ .bottom = options.padding};
172
+
173
+ ShadowView view;
174
+ view.componentName = "TextInput";
175
+ view.surfaceId = kSurfaceId;
176
+ view.tag = tag;
177
+ view.props = parsed;
178
+ view.layoutMetrics = metrics;
179
+ return view;
180
+ }
181
+
182
+ void apply(Win32MountingManager &manager, ShadowViewMutationList &&mutations) {
183
+ manager.applyTransaction(
184
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
185
+ }
186
+
187
+ void mount(Win32MountingManager &manager, Tag parent, const ShadowView &shadowView) {
188
+ ShadowViewMutationList mutations;
189
+ mutations.push_back(ShadowViewMutation::CreateMutation(shadowView));
190
+ mutations.push_back(ShadowViewMutation::InsertMutation(parent, shadowView, 0));
191
+ apply(manager, std::move(mutations));
192
+ }
193
+
194
+ void update(Win32MountingManager &manager, const ShadowView &before, const ShadowView &after) {
195
+ ShadowViewMutationList mutations;
196
+ mutations.push_back(ShadowViewMutation::UpdateMutation(before, after, kSurfaceId));
197
+ apply(manager, std::move(mutations));
198
+ }
199
+
200
+ // A manager already pointed at the hidden window, which is what every test
201
+ // below needs before it mounts anything: `update` creates the peer on first
202
+ // sight and only then.
203
+ std::unique_ptr<Win32MountingManager> makeManager() {
204
+ auto manager = std::make_unique<Win32MountingManager>();
205
+ manager->setHostWindow(testHostWindow());
206
+ gCurrentManager = manager.get();
207
+ return manager;
208
+ }
209
+
210
+ // The peer of a mounted field, found the way the host's WM_COMMAND handler
211
+ // finds one: by walking the host window's children. Nothing exposes the HWND,
212
+ // and nothing should -- so the test goes the long way round rather than the
213
+ // manager growing an accessor for it.
214
+ HWND peerOf(RnWin32View *view) {
215
+ struct Search {
216
+ RECT wanted{};
217
+ HWND found{nullptr};
218
+ } search;
219
+ // The peer is placed inside the view's box, so a child window whose rectangle
220
+ // is within it is the one. Unambiguous here because each test mounts fields
221
+ // that do not overlap.
222
+ search.wanted = RECT{static_cast<LONG>(view->frame().x),
223
+ static_cast<LONG>(view->frame().y),
224
+ static_cast<LONG>(view->frame().x + view->frame().width),
225
+ static_cast<LONG>(view->frame().y + view->frame().height)};
226
+
227
+ EnumChildWindows(
228
+ testHostWindow(),
229
+ [](HWND child, LPARAM data) -> BOOL {
230
+ auto *state = reinterpret_cast<Search *>(data);
231
+ RECT rect{};
232
+ GetWindowRect(child, &rect);
233
+ MapWindowPoints(HWND_DESKTOP, GetParent(child), reinterpret_cast<LPPOINT>(&rect), 2);
234
+ if (rect.left >= state->wanted.left && rect.right <= state->wanted.right &&
235
+ rect.top >= state->wanted.top && rect.bottom <= state->wanted.bottom) {
236
+ state->found = child;
237
+ return FALSE;
238
+ }
239
+ return TRUE;
240
+ },
241
+ reinterpret_cast<LPARAM>(&search));
242
+ return search.found;
243
+ }
244
+
245
+ std::string textOf(HWND control) {
246
+ if (control == nullptr) {
247
+ return "<no control>";
248
+ }
249
+ const int length = GetWindowTextLength(control);
250
+ std::wstring text(static_cast<size_t>(length) + 1, L'\0');
251
+ GetWindowText(control, text.data(), length + 1);
252
+ text.resize(static_cast<size_t>(length));
253
+ return narrow(text);
254
+ }
255
+
256
+ // WM_CHAR per character, which is what a keyboard driver sends and what the
257
+ // scripted BASALT_TEST_TYPE sends.
258
+ void type(HWND control, const std::wstring &text) {
259
+ for (const wchar_t unit : text) {
260
+ SendMessage(control, WM_CHAR, static_cast<WPARAM>(unit), 1);
261
+ }
262
+ }
263
+
264
+ RECT rectOf(HWND control) {
265
+ RECT rect{};
266
+ GetWindowRect(control, &rect);
267
+ MapWindowPoints(HWND_DESKTOP, GetParent(control), reinterpret_cast<LPPOINT>(&rect), 2);
268
+ return rect;
269
+ }
270
+
271
+ } // namespace
272
+
273
+ // A mounted field gets a real control, and the host's window is its parent.
274
+ TEST(win32_a_text_input_mounts_a_real_control) {
275
+ auto manager = makeManager();
276
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
277
+ root->setFrame(0, 0, 800, 600);
278
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44));
279
+ manager->syncTextInputBounds(root);
280
+
281
+ HWND control = peerOf(manager->viewForTag(10));
282
+ EXPECT(control != nullptr);
283
+ EXPECT(GetParent(control) == testHostWindow());
284
+
285
+ manager->destroySurfaceRoot(kSurfaceId);
286
+ }
287
+
288
+ // The `text` prop reaches the control, which is what a `defaultValue` or a
289
+ // controlled field's first render looks like from here.
290
+ TEST(win32_the_text_prop_reaches_the_control) {
291
+ auto manager = makeManager();
292
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
293
+ root->setFrame(0, 0, 800, 600);
294
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44, {.text = "hello"}));
295
+ manager->syncTextInputBounds(root);
296
+
297
+ EXPECT_EQ(textOf(peerOf(manager->viewForTag(10))), std::string("hello"));
298
+
299
+ manager->destroySurfaceRoot(kSurfaceId);
300
+ }
301
+
302
+ // The controlled loop, in the shape js/input.js exercises: the user types, and
303
+ // JavaScript sends back something *different*. The field has to take it.
304
+ TEST(win32_a_changed_text_prop_is_applied) {
305
+ auto manager = makeManager();
306
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
307
+ root->setFrame(0, 0, 800, 600);
308
+ const ShadowView before = makeField(10, 20, 30, 200, 44);
309
+ mount(*manager, kSurfaceId, before);
310
+ manager->syncTextInputBounds(root);
311
+
312
+ HWND control = peerOf(manager->viewForTag(10));
313
+ type(control, L"hi");
314
+ EXPECT_EQ(textOf(control), std::string("hi"));
315
+
316
+ // React re-renders with the upper-cased value, acknowledging both keystrokes.
317
+ update(*manager,
318
+ before,
319
+ makeField(10, 20, 30, 200, 44, {.text = "HI", .mostRecentEventCount = 2}));
320
+ EXPECT_EQ(textOf(control), std::string("HI"));
321
+
322
+ manager->destroySurfaceRoot(kSurfaceId);
323
+ }
324
+
325
+ // An uncontrolled field keeps what was typed into it. React Native re-sends
326
+ // `mostRecentEventCount` on every change, which is an Update mutation on its
327
+ // own, and `text` for an uncontrolled field is the empty string forever -- so a
328
+ // manager that applies the prop whenever it differs from the control wipes the
329
+ // field on the user's second keystroke. Applying it only when the *prop*
330
+ // changes is what makes this work, and this is the test that says so.
331
+ TEST(win32_an_uncontrolled_field_keeps_what_was_typed) {
332
+ auto manager = makeManager();
333
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
334
+ root->setFrame(0, 0, 800, 600);
335
+ const ShadowView before = makeField(10, 20, 30, 200, 44);
336
+ mount(*manager, kSurfaceId, before);
337
+ manager->syncTextInputBounds(root);
338
+
339
+ HWND control = peerOf(manager->viewForTag(10));
340
+ type(control, L"abc");
341
+ EXPECT_EQ(textOf(control), std::string("abc"));
342
+
343
+ // The re-render an uncontrolled field produces: no text, a bumped count.
344
+ update(*manager, before, makeField(10, 20, 30, 200, 44, {.mostRecentEventCount = 3}));
345
+ EXPECT_EQ(textOf(control), std::string("abc"));
346
+
347
+ manager->destroySurfaceRoot(kSurfaceId);
348
+ }
349
+
350
+ // A prop older than what the user has since typed is dropped. That is what
351
+ // React Native counts events for, and without it a fast typist watches
352
+ // characters reorder themselves.
353
+ TEST(win32_a_stale_text_prop_is_dropped) {
354
+ auto manager = makeManager();
355
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
356
+ root->setFrame(0, 0, 800, 600);
357
+ const ShadowView before = makeField(10, 20, 30, 200, 44);
358
+ mount(*manager, kSurfaceId, before);
359
+ manager->syncTextInputBounds(root);
360
+
361
+ HWND control = peerOf(manager->viewForTag(10));
362
+ type(control, L"abcd");
363
+
364
+ // JavaScript answering the first keystroke, three keystrokes late.
365
+ update(*manager,
366
+ before,
367
+ makeField(10, 20, 30, 200, 44, {.text = "A", .mostRecentEventCount = 1}));
368
+ EXPECT_EQ(textOf(control), std::string("abcd"));
369
+
370
+ // And once it has caught up, the same value is applied rather than lost.
371
+ update(*manager,
372
+ before,
373
+ makeField(10, 20, 30, 200, 44, {.text = "A", .mostRecentEventCount = 4}));
374
+ EXPECT_EQ(textOf(control), std::string("A"));
375
+
376
+ manager->destroySurfaceRoot(kSurfaceId);
377
+ }
378
+
379
+ // Enter is swallowed rather than inserted. A single-line EDIT has nowhere to
380
+ // put a newline and beeps at one; that beep is an unhandled submitEditing.
381
+ TEST(win32_enter_does_not_reach_the_text) {
382
+ auto manager = makeManager();
383
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
384
+ root->setFrame(0, 0, 800, 600);
385
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44));
386
+ manager->syncTextInputBounds(root);
387
+
388
+ HWND control = peerOf(manager->viewForTag(10));
389
+ type(control, L"a\rb\tc\x1b");
390
+ EXPECT_EQ(textOf(control), std::string("abc"));
391
+
392
+ manager->destroySurfaceRoot(kSurfaceId);
393
+ }
394
+
395
+ // editable: false, which React Native also spells readOnly.
396
+ TEST(win32_a_read_only_field_refuses_typing) {
397
+ auto manager = makeManager();
398
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
399
+ root->setFrame(0, 0, 800, 600);
400
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44, {.editable = false}));
401
+ manager->syncTextInputBounds(root);
402
+
403
+ HWND control = peerOf(manager->viewForTag(10));
404
+ type(control, L"nope");
405
+ EXPECT_EQ(textOf(control), std::string(""));
406
+
407
+ manager->destroySurfaceRoot(kSurfaceId);
408
+ }
409
+
410
+ // secureTextEntry, which is a message here rather than the different class
411
+ // AppKit needs -- so unlike macOS it can be turned on and off without losing
412
+ // what was typed, and this checks that as well as that it is on at all.
413
+ TEST(win32_secure_text_entry_masks_and_can_be_turned_off) {
414
+ auto manager = makeManager();
415
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
416
+ root->setFrame(0, 0, 800, 600);
417
+ const ShadowView before = makeField(10, 20, 30, 200, 44, {.secure = true});
418
+ mount(*manager, kSurfaceId, before);
419
+ manager->syncTextInputBounds(root);
420
+
421
+ HWND control = peerOf(manager->viewForTag(10));
422
+ type(control, L"secret");
423
+ EXPECT(SendMessage(control, EM_GETPASSWORDCHAR, 0, 0) != 0);
424
+ // Masked on screen and not in the buffer: the value React sees is the real
425
+ // one, which is the whole point of a password field rather than a cipher.
426
+ EXPECT_EQ(textOf(control), std::string("secret"));
427
+
428
+ update(*manager, before, makeField(10, 20, 30, 200, 44, {.secure = false}));
429
+ EXPECT(SendMessage(control, EM_GETPASSWORDCHAR, 0, 0) == 0);
430
+ EXPECT_EQ(textOf(control), std::string("secret"));
431
+
432
+ manager->destroySurfaceRoot(kSurfaceId);
433
+ }
434
+
435
+ // maxLength, which macOS does not have at all: an NSTextField has no maximum
436
+ // and enforcing one there needs a formatter.
437
+ TEST(win32_max_length_is_enforced) {
438
+ auto manager = makeManager();
439
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
440
+ root->setFrame(0, 0, 800, 600);
441
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44, {.maxLength = 3}));
442
+ manager->syncTextInputBounds(root);
443
+
444
+ HWND control = peerOf(manager->viewForTag(10));
445
+ type(control, L"abcdef");
446
+ EXPECT_EQ(textOf(control), std::string("abc"));
447
+
448
+ manager->destroySurfaceRoot(kSurfaceId);
449
+ }
450
+
451
+ // The peer is placed inside the content insets, so that `paddingHorizontal` on
452
+ // a field means what it means on a <View>, and no taller than one line so the
453
+ // text sits in the middle rather than at the top.
454
+ TEST(win32_the_peer_sits_inside_the_padding_and_is_centred) {
455
+ auto manager = makeManager();
456
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
457
+ root->setFrame(0, 0, 800, 600);
458
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44, {.padding = 12}));
459
+ manager->syncTextInputBounds(root);
460
+
461
+ const RECT rect = rectOf(peerOf(manager->viewForTag(10)));
462
+ EXPECT_EQ(static_cast<long>(rect.left), 32L);
463
+ EXPECT_EQ(static_cast<long>(rect.right), 208L);
464
+ // Vertically centred in the 20..64 box, whatever the measured line height
465
+ // turned out to be. Asserting the centring rather than the height is what
466
+ // keeps this from failing on a machine whose default font is a point taller.
467
+ EXPECT_EQ(static_cast<long>((rect.top - 30) - (74 - rect.bottom)), 0L);
468
+ EXPECT(rect.bottom - rect.top < 44);
469
+
470
+ manager->destroySurfaceRoot(kSurfaceId);
471
+ }
472
+
473
+ // A peer follows its view. Nothing in the view tree owns a window, so it has to
474
+ // be moved by hand -- which is also why it is worth a test that it is.
475
+ TEST(win32_the_peer_follows_its_view) {
476
+ auto manager = makeManager();
477
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
478
+ root->setFrame(0, 0, 800, 600);
479
+ mount(*manager, kSurfaceId, makeView(10, 0, 0, 400, 400));
480
+ mount(*manager, 10, makeField(11, 0, 0, 200, 44));
481
+ manager->syncTextInputBounds(root);
482
+
483
+ HWND control = peerOf(manager->viewForTag(11));
484
+ EXPECT_EQ(static_cast<long>(rectOf(control).left), 0L);
485
+
486
+ // The *parent* moves, which produces no mutation for the field at all.
487
+ manager->viewForTag(10)->setFrame(100, 50, 400, 400);
488
+ manager->syncTextInputBounds(root);
489
+ EXPECT_EQ(static_cast<long>(rectOf(control).left), 100L);
490
+
491
+ manager->destroySurfaceRoot(kSurfaceId);
492
+ }
493
+
494
+ // A field scrolled out of its list is hidden. A child window is not clipped by
495
+ // anything in the React tree, so without this it goes on being drawn over the
496
+ // list's neighbours -- which is the most visible consequence of the peer being
497
+ // a window.
498
+ TEST(win32_a_peer_scrolled_out_of_view_is_hidden) {
499
+ auto manager = makeManager();
500
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
501
+ root->setFrame(0, 0, 800, 600);
502
+ mount(*manager, kSurfaceId, makeView(10, 0, 0, 400, 100));
503
+ manager->viewForTag(10)->setClipsChildren(true);
504
+ mount(*manager, 10, makeField(11, 0, 0, 200, 44));
505
+ manager->syncTextInputBounds(root);
506
+
507
+ HWND control = peerOf(manager->viewForTag(11));
508
+ EXPECT(IsWindowVisible(control) || GetParent(control) != nullptr);
509
+
510
+ manager->viewForTag(10)->setScrollOffset(0, 500);
511
+ manager->syncTextInputBounds(root);
512
+ EXPECT(!IsWindowVisible(control));
513
+
514
+ manager->viewForTag(10)->setScrollOffset(0, 0);
515
+ manager->syncTextInputBounds(root);
516
+ EXPECT_EQ(static_cast<long>(rectOf(control).left), 0L);
517
+
518
+ manager->destroySurfaceRoot(kSurfaceId);
519
+ }
520
+
521
+ // focus, blur and setTextAndSelection, which arrive through dispatchCommand.
522
+ TEST(win32_the_commands_reach_the_control) {
523
+ auto manager = makeManager();
524
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
525
+ root->setFrame(0, 0, 800, 600);
526
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44));
527
+ manager->syncTextInputBounds(root);
528
+ HWND control = peerOf(manager->viewForTag(10));
529
+
530
+ manager->applyCommand(10, "setTextAndSelection", folly::dynamic::array(0, "typed", 1, 3));
531
+ EXPECT_EQ(textOf(control), std::string("typed"));
532
+
533
+ DWORD start = 0;
534
+ DWORD end = 0;
535
+ SendMessage(
536
+ control, EM_GETSEL, reinterpret_cast<WPARAM>(&start), reinterpret_cast<LPARAM>(&end));
537
+ EXPECT_EQ(static_cast<long>(start), 1L);
538
+ EXPECT_EQ(static_cast<long>(end), 3L);
539
+
540
+ // Focus needs the window to be able to take it, and a WS_POPUP that was never
541
+ // shown cannot -- so what is checked is that the command is claimed and
542
+ // survives, not that focus moved. The end-to-end run covers the rest.
543
+ manager->applyCommand(10, "focus", folly::dynamic::array());
544
+ manager->applyCommand(10, "blur", folly::dynamic::array());
545
+ EXPECT_EQ(textOf(control), std::string("typed"));
546
+
547
+ manager->destroySurfaceRoot(kSurfaceId);
548
+ }
549
+
550
+ // A stale setTextAndSelection is dropped for the same reason a stale prop is.
551
+ TEST(win32_a_stale_set_text_command_is_dropped) {
552
+ auto manager = makeManager();
553
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
554
+ root->setFrame(0, 0, 800, 600);
555
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44));
556
+ manager->syncTextInputBounds(root);
557
+ HWND control = peerOf(manager->viewForTag(10));
558
+
559
+ type(control, L"abc");
560
+ manager->applyCommand(10, "setTextAndSelection", folly::dynamic::array(1, "stale", 0, 0));
561
+ EXPECT_EQ(textOf(control), std::string("abc"));
562
+
563
+ manager->destroySurfaceRoot(kSurfaceId);
564
+ }
565
+
566
+ // The peer goes with the view. A control left behind would keep drawing over
567
+ // the surface that replaced it, which is the failure a plain C++ view layer
568
+ // cannot get for free.
569
+ TEST(win32_deleting_a_text_input_destroys_its_peer) {
570
+ auto manager = makeManager();
571
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
572
+ root->setFrame(0, 0, 800, 600);
573
+ const ShadowView field = makeField(10, 20, 30, 200, 44);
574
+ mount(*manager, kSurfaceId, field);
575
+ manager->syncTextInputBounds(root);
576
+
577
+ HWND control = peerOf(manager->viewForTag(10));
578
+ EXPECT(IsWindow(control));
579
+
580
+ ShadowViewMutationList mutations;
581
+ mutations.push_back(ShadowViewMutation::RemoveMutation(kSurfaceId, field, 0));
582
+ mutations.push_back(ShadowViewMutation::DeleteMutation(field));
583
+ apply(*manager, std::move(mutations));
584
+
585
+ EXPECT(!IsWindow(control));
586
+ EXPECT(manager->viewForTag(10) == nullptr);
587
+ // And a command that arrives after it is gone is survivable, which is the
588
+ // shape of a blur racing a surface teardown.
589
+ manager->applyCommand(10, "blur", folly::dynamic::array());
590
+
591
+ manager->destroySurfaceRoot(kSurfaceId);
592
+ }
593
+
594
+ // The manager outlives nothing: every peer goes when it does. Without this a
595
+ // second surface in the same process inherits the first one's controls.
596
+ TEST(win32_destroying_the_manager_destroys_every_peer) {
597
+ HWND control = nullptr;
598
+ {
599
+ auto manager = makeManager();
600
+ RnWin32View *root = manager->createSurfaceRoot(kSurfaceId);
601
+ root->setFrame(0, 0, 800, 600);
602
+ mount(*manager, kSurfaceId, makeField(10, 20, 30, 200, 44));
603
+ manager->syncTextInputBounds(root);
604
+ control = peerOf(manager->viewForTag(10));
605
+ EXPECT(IsWindow(control));
606
+ manager->destroySurfaceRoot(kSurfaceId);
607
+ }
608
+ EXPECT(!IsWindow(control));
609
+ }