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,271 @@
1
+ // What a screen reader is told.
2
+ //
3
+ // The union of tests/test_accessibility.cpp on GTK and
4
+ // tests/test_appkit_accessibility.mm on AppKit. What differs is how the answer
5
+ // is obtained: those two set a property on a widget and read it back, while
6
+ // UI Automation is a pull interface, so these build the provider and ask it the
7
+ // same questions a screen reader would -- by property id, through
8
+ // GetPropertyValue.
9
+ //
10
+ // Nothing here needs a window. UIA's tree navigation does, because a fragment
11
+ // root is an HWND, and that half lands with the host; the property half is
12
+ // answerable now and is most of what the other two suites check.
13
+
14
+ #include "TestHarness.h"
15
+
16
+ #include "RnWin32Accessible.h"
17
+ #include "RnWin32View.h"
18
+ // For narrow(): UIA answers in BSTRs and these assertions are written in UTF-8,
19
+ // like the rest of the project.
20
+ #include "Win32Strings.h"
21
+
22
+ #include <windows.h>
23
+
24
+ // WIN32_LEAN_AND_MEAN keeps combaseapi.h out of windows.h, and that is where the
25
+ // `interface` macro lives -- without it UIAutomationCore.h fails on its own
26
+ // first typedef, which reads as a broken SDK rather than a missing include.
27
+ #include <objbase.h>
28
+
29
+ #include <uiautomation.h>
30
+ #include <wrl/client.h>
31
+
32
+ #include <memory>
33
+ #include <sstream>
34
+ #include <string>
35
+
36
+ using Microsoft::WRL::ComPtr;
37
+ using basalt::win32::RnAccessibleFlag;
38
+ using basalt::win32::RnAccessibleInfo;
39
+ using basalt::win32::RnWin32View;
40
+
41
+ namespace {
42
+
43
+ ComPtr<IRawElementProviderSimple> providerFor(const RnAccessibleInfo &info) {
44
+ ComPtr<IRawElementProviderSimple> provider;
45
+ provider.Attach(basalt::win32::createAccessibleProvider(info));
46
+ return provider;
47
+ }
48
+
49
+ // The control type a provider reports, or -1 if it has none.
50
+ long controlTypeOf(const ComPtr<IRawElementProviderSimple> &provider) {
51
+ if (!provider) {
52
+ return -1;
53
+ }
54
+ VARIANT value;
55
+ VariantInit(&value);
56
+ if (FAILED(provider->GetPropertyValue(UIA_ControlTypePropertyId, &value))) {
57
+ return -1;
58
+ }
59
+ const long result = value.vt == VT_I4 ? value.lVal : -1;
60
+ VariantClear(&value);
61
+ return result;
62
+ }
63
+
64
+ // A string property, or "" when the provider declined to answer.
65
+ std::string stringProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
66
+ if (!provider) {
67
+ return {};
68
+ }
69
+ VARIANT value;
70
+ VariantInit(&value);
71
+ if (FAILED(provider->GetPropertyValue(id, &value))) {
72
+ return {};
73
+ }
74
+ std::string result;
75
+ if (value.vt == VT_BSTR && value.bstrVal != nullptr) {
76
+ const std::wstring wide(value.bstrVal, SysStringLen(value.bstrVal));
77
+ result = basalt::win32::narrow(wide);
78
+ }
79
+ VariantClear(&value);
80
+ return result;
81
+ }
82
+
83
+ // A VARIANT's type tag, so a test can say "this property was left unset" --
84
+ // which is a different claim from "this property is false".
85
+ VARTYPE propertyType(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
86
+ if (!provider) {
87
+ return VT_EMPTY;
88
+ }
89
+ VARIANT value;
90
+ VariantInit(&value);
91
+ if (FAILED(provider->GetPropertyValue(id, &value))) {
92
+ return VT_EMPTY;
93
+ }
94
+ const VARTYPE type = value.vt;
95
+ VariantClear(&value);
96
+ return type;
97
+ }
98
+
99
+ long longProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
100
+ VARIANT value;
101
+ VariantInit(&value);
102
+ provider->GetPropertyValue(id, &value);
103
+ const long result = value.vt == VT_I4 ? value.lVal : -1;
104
+ VariantClear(&value);
105
+ return result;
106
+ }
107
+
108
+ bool boolProperty(const ComPtr<IRawElementProviderSimple> &provider, PROPERTYID id) {
109
+ VARIANT value;
110
+ VariantInit(&value);
111
+ provider->GetPropertyValue(id, &value);
112
+ const bool result = value.vt == VT_BOOL && value.boolVal == VARIANT_TRUE;
113
+ VariantClear(&value);
114
+ return result;
115
+ }
116
+
117
+ RnAccessibleInfo withRole(const std::string &role) {
118
+ RnAccessibleInfo info;
119
+ info.role = role;
120
+ return info;
121
+ }
122
+
123
+ } // namespace
124
+
125
+ TEST(accessibility_maps_react_native_roles_onto_uia) {
126
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("button"))),
127
+ static_cast<long>(UIA_ButtonControlTypeId));
128
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("link"))),
129
+ static_cast<long>(UIA_HyperlinkControlTypeId));
130
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("image"))),
131
+ static_cast<long>(UIA_ImageControlTypeId));
132
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("text"))),
133
+ static_cast<long>(UIA_TextControlTypeId));
134
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("checkbox"))),
135
+ static_cast<long>(UIA_CheckBoxControlTypeId));
136
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("adjustable"))),
137
+ static_cast<long>(UIA_SliderControlTypeId));
138
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("tablist"))),
139
+ static_cast<long>(UIA_TabControlTypeId));
140
+ }
141
+
142
+ TEST(accessibility_falls_back_to_a_group) {
143
+ // A role UIA has no control type for is a group, not a guess. A wrong control
144
+ // type is worse than a vague one: it makes a view announce itself as
145
+ // something it is not.
146
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("summary"))),
147
+ static_cast<long>(UIA_GroupControlTypeId));
148
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("somethingnobodyhasheardof"))),
149
+ static_cast<long>(UIA_GroupControlTypeId));
150
+
151
+ // And a switch is a checkbox, because UIA has no switch. Named here rather
152
+ // than left to be rediscovered, since it is a deliberate approximation.
153
+ EXPECT_EQ(controlTypeOf(providerFor(withRole("switch"))),
154
+ static_cast<long>(UIA_CheckBoxControlTypeId));
155
+ }
156
+
157
+ TEST(accessibility_leaves_plain_views_out_of_the_tree) {
158
+ // A <View> with no role, no label and no hint is scaffolding. A tree full of
159
+ // untyped groups is worse for a screen reader user than a tree without them.
160
+ RnAccessibleInfo plain;
161
+ EXPECT(!basalt::win32::isAccessibilityElement(plain));
162
+ EXPECT(basalt::win32::createAccessibleProvider(plain) == nullptr);
163
+
164
+ // A label alone is enough to make it worth reporting.
165
+ RnAccessibleInfo labelled;
166
+ labelled.label = "Close";
167
+ EXPECT(basalt::win32::isAccessibilityElement(labelled));
168
+ }
169
+
170
+ TEST(accessibility_honours_a_presentational_role) {
171
+ EXPECT(basalt::win32::roleIsPresentational("none"));
172
+ EXPECT(basalt::win32::roleIsPresentational("presentation"));
173
+ EXPECT(!basalt::win32::roleIsPresentational("button"));
174
+
175
+ // Explicitly presentational beats having a label: the app said not to report
176
+ // this one.
177
+ RnAccessibleInfo info = withRole("none");
178
+ info.label = "ignored";
179
+ EXPECT(basalt::win32::createAccessibleProvider(info) == nullptr);
180
+ }
181
+
182
+ TEST(accessibility_label_and_hint_reach_uia) {
183
+ RnAccessibleInfo info = withRole("button");
184
+ info.label = "Save";
185
+ info.hint = "Writes the file to disk";
186
+
187
+ auto provider = providerFor(info);
188
+ EXPECT(provider != nullptr);
189
+ EXPECT_EQ(stringProperty(provider, UIA_NamePropertyId), std::string("Save"));
190
+ // React Native's hint is "what happens if you do this", which is what UIA
191
+ // calls help text.
192
+ EXPECT_EQ(stringProperty(provider, UIA_HelpTextPropertyId),
193
+ std::string("Writes the file to disk"));
194
+ }
195
+
196
+ TEST(accessibility_states_reach_uia) {
197
+ RnAccessibleInfo info = withRole("checkbox");
198
+ info.state.disabled = RnAccessibleFlag::True;
199
+ info.state.checked = RnAccessibleFlag::True;
200
+ info.state.selected = RnAccessibleFlag::True;
201
+ info.state.expanded = RnAccessibleFlag::False;
202
+
203
+ auto provider = providerFor(info);
204
+ EXPECT(provider != nullptr);
205
+
206
+ EXPECT(!boolProperty(provider, UIA_IsEnabledPropertyId));
207
+ EXPECT_EQ(longProperty(provider, UIA_ToggleToggleStatePropertyId),
208
+ static_cast<long>(ToggleState_On));
209
+ EXPECT(boolProperty(provider, UIA_SelectionItemIsSelectedPropertyId));
210
+ EXPECT_EQ(longProperty(provider, UIA_ExpandCollapseExpandCollapseStatePropertyId),
211
+ static_cast<long>(ExpandCollapseState_Collapsed));
212
+ }
213
+
214
+ TEST(accessibility_unset_leaves_uia_alone) {
215
+ // The tri-state, stated as a test. A view that never mentions being checked
216
+ // is not an unchecked checkbox, and must not report ToggleState_Off -- which
217
+ // a screen reader would read aloud as "not checked".
218
+ auto provider = providerFor(withRole("button"));
219
+ EXPECT(provider != nullptr);
220
+
221
+ EXPECT_EQ(propertyType(provider, UIA_ToggleToggleStatePropertyId), VT_EMPTY);
222
+ EXPECT_EQ(propertyType(provider, UIA_SelectionItemIsSelectedPropertyId), VT_EMPTY);
223
+ EXPECT_EQ(propertyType(provider, UIA_ExpandCollapseExpandCollapseStatePropertyId), VT_EMPTY);
224
+
225
+ // IsEnabled is the exception, and deliberately: UIA has no "unknown" for it,
226
+ // and its default is enabled, which is React Native's default too.
227
+ EXPECT_EQ(propertyType(provider, UIA_IsEnabledPropertyId), VT_BOOL);
228
+ EXPECT(boolProperty(provider, UIA_IsEnabledPropertyId));
229
+ }
230
+
231
+ TEST(accessibility_hidden_takes_a_view_out_of_the_tree) {
232
+ RnAccessibleInfo info = withRole("button");
233
+ info.label = "Save";
234
+ info.hidden = true;
235
+
236
+ EXPECT(!basalt::win32::isAccessibilityElement(info));
237
+ EXPECT(basalt::win32::createAccessibleProvider(info) == nullptr);
238
+ }
239
+
240
+ TEST(accessibility_role_is_reported_in_the_tree) {
241
+ auto view = std::make_unique<RnWin32View>(3);
242
+ view->setFrame(0, 0, 100, 40);
243
+
244
+ RnAccessibleInfo info = withRole("button");
245
+ info.label = "Save";
246
+ view->setAccessibleInfo(info);
247
+
248
+ // React Native's role name, not UIA's -- the same string GTK and AppKit
249
+ // print, so the three dumps compare.
250
+ EXPECT_EQ(view->describeTree(), std::string("view tag=3 frame=(0,0 100x40) role=button\n"));
251
+ }
252
+
253
+ TEST(accessibility_role_can_change_after_mount) {
254
+ // GTK cannot do this: a GtkAccessible's role is construct-only, so
255
+ // `docs/DECISIONS.md` records that accessibilityRole is fixed once a widget
256
+ // exists. UI Automation pulls rather than being pushed to, so there is
257
+ // nothing baked in, and the mounting manager should not copy GTK's
258
+ // restriction when it arrives.
259
+ auto view = std::make_unique<RnWin32View>(4);
260
+ view->setAccessibleInfo(withRole("button"));
261
+
262
+ ComPtr<IRawElementProviderSimple> asButton;
263
+ asButton.Attach(view->createAccessibleProvider());
264
+ EXPECT_EQ(controlTypeOf(asButton), static_cast<long>(UIA_ButtonControlTypeId));
265
+
266
+ view->setAccessibleInfo(withRole("checkbox"));
267
+
268
+ ComPtr<IRawElementProviderSimple> asCheckbox;
269
+ asCheckbox.Attach(view->createAccessibleProvider());
270
+ EXPECT_EQ(controlTypeOf(asCheckbox), static_cast<long>(UIA_CheckBoxControlTypeId));
271
+ }
@@ -0,0 +1,121 @@
1
+ // Tests for the clipboard half of core/PlatformServices.h on Windows.
2
+ //
3
+ // These use the real clipboard, because the thing being tested is how it
4
+ // behaves when somebody else holds it -- and the only somebody that can be
5
+ // arranged from a test is a second thread in this process, which OpenClipboard
6
+ // treats exactly like another program. What the clipboard held before is put
7
+ // back at the end, as text; anything richer than text is lost, the same as
8
+ // js/modules.js running.
9
+
10
+ #include "TestHarness.h"
11
+
12
+ #include "PlatformServices.h"
13
+
14
+ #include <windows.h>
15
+
16
+ #include <atomic>
17
+ #include <chrono>
18
+ #include <string>
19
+ #include <thread>
20
+
21
+ namespace {
22
+
23
+ // Opens the clipboard on another thread and keeps it for `holdMs`, which is
24
+ // what a clipboard listener does right after every write. Returns once the
25
+ // clipboard is actually held, so the caller's attempt is guaranteed to collide.
26
+ class ClipboardHolder {
27
+ public:
28
+ explicit ClipboardHolder(int holdMs) {
29
+ thread_ = std::thread([this, holdMs] {
30
+ // Opened against a window, because only that excludes anyone. A holder
31
+ // that passes no window -- as the code under test does -- blocks nobody,
32
+ // not even another process, and the first version of this test held the
33
+ // clipboard that way and passed with the retry removed. A clipboard
34
+ // listener has a window by definition; that is how it is told.
35
+ const HWND window = CreateWindowExW(
36
+ 0, L"STATIC", L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
37
+ // Retried, and for longer than the code under test retries: the write
38
+ // just before this is announced to every listener on the machine, and
39
+ // one of them is usually still reading.
40
+ for (int attempt = 0; attempt < 200 && !held_; ++attempt) {
41
+ if (OpenClipboard(window)) {
42
+ held_ = true;
43
+ } else {
44
+ Sleep(5);
45
+ }
46
+ }
47
+ if (held_) {
48
+ std::this_thread::sleep_for(std::chrono::milliseconds(holdMs));
49
+ CloseClipboard();
50
+ }
51
+ if (window != nullptr) {
52
+ DestroyWindow(window);
53
+ }
54
+ finished_ = true;
55
+ });
56
+ // Until it is held, or the attempt to hold it has failed and held() says so.
57
+ while (!held_ && !finished_) {
58
+ std::this_thread::yield();
59
+ }
60
+ }
61
+ ~ClipboardHolder() {
62
+ thread_.join();
63
+ }
64
+ ClipboardHolder(const ClipboardHolder &) = delete;
65
+ ClipboardHolder &operator=(const ClipboardHolder &) = delete;
66
+
67
+ bool held() const {
68
+ return held_;
69
+ }
70
+
71
+ private:
72
+ std::thread thread_;
73
+ std::atomic<bool> held_{false};
74
+ std::atomic<bool> finished_{false};
75
+ };
76
+
77
+ class RestoreClipboard {
78
+ public:
79
+ RestoreClipboard() : saved_(basalt::clipboardText()) {}
80
+ ~RestoreClipboard() {
81
+ basalt::setClipboardText(saved_);
82
+ }
83
+ RestoreClipboard(const RestoreClipboard &) = delete;
84
+ RestoreClipboard &operator=(const RestoreClipboard &) = delete;
85
+
86
+ private:
87
+ std::string saved_;
88
+ };
89
+
90
+ } // namespace
91
+
92
+ TEST(win32_clipboard_round_trips_unicode) {
93
+ RestoreClipboard restore;
94
+ const std::string written = "h\xC3\xA9llo \xE4\xB8\x96\xE7\x95\x8C \xF0\x9F\x9C\x82";
95
+ basalt::setClipboardText(written);
96
+ EXPECT_EQ(basalt::clipboardText(), written);
97
+ }
98
+
99
+ TEST(win32_clipboard_write_waits_out_a_brief_holder) {
100
+ RestoreClipboard restore;
101
+ basalt::setClipboardText("before");
102
+ {
103
+ // Well inside the retry budget, and far longer than one attempt.
104
+ ClipboardHolder holder(20);
105
+ EXPECT(holder.held());
106
+ basalt::setClipboardText("after");
107
+ }
108
+ EXPECT_EQ(basalt::clipboardText(), std::string("after"));
109
+ }
110
+
111
+ TEST(win32_clipboard_read_waits_out_a_brief_holder) {
112
+ RestoreClipboard restore;
113
+ basalt::setClipboardText("still here");
114
+ std::string read;
115
+ {
116
+ ClipboardHolder holder(20);
117
+ EXPECT(holder.held());
118
+ read = basalt::clipboardText();
119
+ }
120
+ EXPECT_EQ(read, std::string("still here"));
121
+ }
@@ -0,0 +1,195 @@
1
+ // Keyboard focus: what Tab stops on, in what order, and what Enter does.
2
+ //
3
+ // The same questions the GTK and AppKit suites ask, in the same order, because
4
+ // the answers have to match -- but here they are testable with no window at
5
+ // all. That is the difference this platform makes: on the other two, focus
6
+ // belongs to the toolkit and a test has to show a window to get at it. Here a
7
+ // React Native view is a C++ object with no window behind it, so the whole
8
+ // chain is Win32Focus.cpp's and can be driven directly.
9
+ //
10
+ // No emitter is attached to these hand-built shadow views, so the focus and
11
+ // blur events have nowhere to go and the click dispatch does nothing. What is
12
+ // observable is the part that decides where focus is, which is the part this
13
+ // project can get wrong on its own.
14
+
15
+ #include "TestHarness.h"
16
+
17
+ #include "Win32Focus.h"
18
+ #include "Win32MountingManager.h"
19
+
20
+ #include <react/renderer/components/view/ViewProps.h>
21
+
22
+ #include <cstdint>
23
+ #include <memory>
24
+ #include <sstream>
25
+
26
+ using basalt::Win32FocusManager;
27
+ using basalt::Win32MountingManager;
28
+ using basalt::win32::RnWin32View;
29
+ using facebook::react::LayoutMetrics;
30
+ using facebook::react::MountingTransaction;
31
+ using facebook::react::ShadowView;
32
+ using facebook::react::ShadowViewMutation;
33
+ using facebook::react::ShadowViewMutationList;
34
+ using facebook::react::SurfaceId;
35
+ using facebook::react::Tag;
36
+ using facebook::react::TransactionTelemetry;
37
+ using facebook::react::ViewProps;
38
+
39
+ namespace {
40
+
41
+ constexpr SurfaceId kSurfaceId = 1;
42
+
43
+ ShadowView makeAccessibleView(Tag tag, float y, bool accessible) {
44
+ auto props = std::make_shared<ViewProps>();
45
+ props->accessible = accessible;
46
+
47
+ LayoutMetrics metrics;
48
+ metrics.frame = {.origin = {.x = 0, .y = y}, .size = {.width = 200, .height = 40}};
49
+
50
+ ShadowView view;
51
+ view.componentName = "View";
52
+ view.surfaceId = kSurfaceId;
53
+ view.tag = tag;
54
+ view.props = props;
55
+ view.layoutMetrics = metrics;
56
+ return view;
57
+ }
58
+
59
+ void mountAll(Win32MountingManager &manager, std::initializer_list<ShadowView> views) {
60
+ ShadowViewMutationList mutations;
61
+ int index = 0;
62
+ for (const ShadowView &view : views) {
63
+ mutations.push_back(ShadowViewMutation::CreateMutation(view));
64
+ mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, view, index++));
65
+ }
66
+ manager.applyTransaction(
67
+ kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
68
+ }
69
+
70
+ } // namespace
71
+
72
+ TEST(win32_focus_accessible_views_are_the_ones_tab_stops_on) {
73
+ Win32MountingManager manager;
74
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
75
+ root->setFrame(0, 0, 400, 400);
76
+ // React Native's `focusable` prop never reaches this platform, so `accessible`
77
+ // is the signal -- and it is what <Pressable> sets on everything it renders.
78
+ mountAll(manager,
79
+ {makeAccessibleView(10, 0, true),
80
+ makeAccessibleView(11, 50, false),
81
+ makeAccessibleView(12, 100, true)});
82
+
83
+ EXPECT(manager.viewForTag(10)->focusable());
84
+ EXPECT(!manager.viewForTag(11)->focusable());
85
+ EXPECT(manager.viewForTag(12)->focusable());
86
+ // And it shows up in the tree dump, so the cross-host diff can say the prop
87
+ // arrived on all three.
88
+ EXPECT(root->describeTree().find("focusable") != std::string::npos);
89
+
90
+ manager.destroySurfaceRoot(kSurfaceId);
91
+ }
92
+
93
+ TEST(win32_focus_tab_visits_focusable_views_in_tree_order_and_wraps) {
94
+ Win32MountingManager manager;
95
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
96
+ root->setFrame(0, 0, 400, 400);
97
+ Win32FocusManager focus(&manager, root);
98
+ mountAll(manager,
99
+ {makeAccessibleView(10, 0, true),
100
+ makeAccessibleView(11, 50, false),
101
+ makeAccessibleView(12, 100, true)});
102
+
103
+ // Nothing is focused when a window opens, on any of the three.
104
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 0L);
105
+
106
+ EXPECT(focus.moveFocus(true));
107
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
108
+ // 11 is not accessible, so Tab goes past it.
109
+ EXPECT(focus.moveFocus(true));
110
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 12L);
111
+ // And round again: a window's Tab order wraps.
112
+ EXPECT(focus.moveFocus(true));
113
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
114
+ // Backwards is the same order in reverse.
115
+ EXPECT(focus.moveFocus(false));
116
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 12L);
117
+
118
+ manager.destroySurfaceRoot(kSurfaceId);
119
+ }
120
+
121
+ TEST(win32_focus_moves_the_ring_with_it) {
122
+ Win32MountingManager manager;
123
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
124
+ root->setFrame(0, 0, 400, 400);
125
+ Win32FocusManager focus(&manager, root);
126
+ mountAll(manager, {makeAccessibleView(10, 0, true), makeAccessibleView(11, 50, true)});
127
+
128
+ // The ring is this project's own here: a view is not a window, so there is no
129
+ // Win32 focus for the painting to ask about.
130
+ focus.moveFocus(true);
131
+ EXPECT(manager.viewForTag(10)->showsFocusRing());
132
+ EXPECT(!manager.viewForTag(11)->showsFocusRing());
133
+
134
+ focus.moveFocus(true);
135
+ EXPECT(!manager.viewForTag(10)->showsFocusRing());
136
+ EXPECT(manager.viewForTag(11)->showsFocusRing());
137
+
138
+ manager.destroySurfaceRoot(kSurfaceId);
139
+ }
140
+
141
+ TEST(win32_focus_a_hidden_view_is_not_a_tab_stop) {
142
+ Win32MountingManager manager;
143
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
144
+ root->setFrame(0, 0, 400, 400);
145
+ Win32FocusManager focus(&manager, root);
146
+ mountAll(manager, {makeAccessibleView(10, 0, true), makeAccessibleView(11, 50, true)});
147
+
148
+ // Tab stopping on something nobody can see is worse than Tab skipping it.
149
+ manager.viewForTag(10)->setHidden(true);
150
+ focus.moveFocus(true);
151
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 11L);
152
+
153
+ manager.destroySurfaceRoot(kSurfaceId);
154
+ }
155
+
156
+ TEST(win32_focus_activating_nothing_is_not_a_click) {
157
+ Win32MountingManager manager;
158
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
159
+ root->setFrame(0, 0, 400, 400);
160
+ Win32FocusManager focus(&manager, root);
161
+ mountAll(manager, {makeAccessibleView(10, 0, true)});
162
+
163
+ // Nothing has focus yet, so there is nothing to activate -- and reporting a
164
+ // press with no target is how a keystroke reaches the wrong view.
165
+ EXPECT(!focus.activateFocused());
166
+
167
+ focus.moveFocus(true);
168
+ // No emitter is attached to these hand-built shadow views, so the click has
169
+ // nowhere to deliver. It must not crash, and the key is still consumed: that
170
+ // is exactly the shape of Enter arriving between a Remove and its Delete.
171
+ EXPECT(focus.activateFocused());
172
+
173
+ manager.destroySurfaceRoot(kSurfaceId);
174
+ }
175
+
176
+ TEST(win32_focus_keys_that_are_not_tab_or_enter_are_left_alone) {
177
+ Win32MountingManager manager;
178
+ RnWin32View *root = manager.createSurfaceRoot(kSurfaceId);
179
+ root->setFrame(0, 0, 400, 400);
180
+ Win32FocusManager focus(&manager, root);
181
+ mountAll(manager, {makeAccessibleView(10, 0, true)});
182
+
183
+ // Reported as unhandled, so the window's own shortcuts still see them. VK_F1
184
+ // and VK_ESCAPE stand for everything that is not this file's business.
185
+ EXPECT(!focus.handleKeyDown(0x70)); // VK_F1
186
+ EXPECT(!focus.handleKeyDown(0x1B)); // VK_ESCAPE
187
+ // And Enter with nothing focused goes on rather than being swallowed.
188
+ EXPECT(!focus.handleKeyDown(0x0D)); // VK_RETURN
189
+
190
+ EXPECT(focus.handleKeyDown(0x09)); // VK_TAB
191
+ EXPECT_EQ(static_cast<long>(focus.focusedTag()), 10L);
192
+ EXPECT(focus.handleKeyDown(0x0D)); // VK_RETURN, now that something has focus
193
+
194
+ manager.destroySurfaceRoot(kSurfaceId);
195
+ }