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.
- package/LICENSE +21 -0
- package/cli/runWindows.js +35 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +398 -0
- package/native/tests/test_win32_accessibility.cpp +271 -0
- package/native/tests/test_win32_clipboard.cpp +121 -0
- package/native/tests/test_win32_focus.cpp +195 -0
- package/native/tests/test_win32_hittest.cpp +410 -0
- package/native/tests/test_win32_image.cpp +239 -0
- package/native/tests/test_win32_imageloader.cpp +208 -0
- package/native/tests/test_win32_input.cpp +361 -0
- package/native/tests/test_win32_key_events.cpp +84 -0
- package/native/tests/test_win32_mounting.cpp +410 -0
- package/native/tests/test_win32_paint.cpp +505 -0
- package/native/tests/test_win32_scrollview.cpp +385 -0
- package/native/tests/test_win32_text.cpp +258 -0
- package/native/tests/test_win32_textinput.cpp +609 -0
- package/native/tests/test_win32_titlebar.cpp +111 -0
- package/native/tests/test_win32_titlebar_actions.cpp +62 -0
- package/native/tests/test_win32_view.cpp +222 -0
- package/native/win32/ColorSchemeWin32.cpp +56 -0
- package/native/win32/ComponentRegistryWin32.cpp +124 -0
- package/native/win32/DirectWriteLayout.cpp +116 -0
- package/native/win32/DirectWriteLayout.h +36 -0
- package/native/win32/DirectWriteLayoutManager.cpp +124 -0
- package/native/win32/FontRegistryDirectWrite.cpp +117 -0
- package/native/win32/PlatformServicesWin32.cpp +643 -0
- package/native/win32/RnWin32Accessible.cpp +258 -0
- package/native/win32/RnWin32Accessible.h +91 -0
- package/native/win32/RnWin32Image.cpp +265 -0
- package/native/win32/RnWin32Image.h +93 -0
- package/native/win32/RnWin32TextLayout.cpp +310 -0
- package/native/win32/RnWin32TextLayout.h +160 -0
- package/native/win32/RnWin32View.cpp +1281 -0
- package/native/win32/RnWin32View.h +506 -0
- package/native/win32/Win32AnimationChoreographer.cpp +76 -0
- package/native/win32/Win32AnimationChoreographer.h +61 -0
- package/native/win32/Win32Clip.h +173 -0
- package/native/win32/Win32DropTarget.cpp +567 -0
- package/native/win32/Win32DropTarget.h +42 -0
- package/native/win32/Win32Focus.cpp +262 -0
- package/native/win32/Win32Focus.h +109 -0
- package/native/win32/Win32ImageLoader.cpp +167 -0
- package/native/win32/Win32ImageLoader.h +104 -0
- package/native/win32/Win32KeyEvents.cpp +126 -0
- package/native/win32/Win32KeyEvents.h +20 -0
- package/native/win32/Win32MenuBar.cpp +352 -0
- package/native/win32/Win32MenuBar.h +16 -0
- package/native/win32/Win32MountingManager.cpp +727 -0
- package/native/win32/Win32MountingManager.h +203 -0
- package/native/win32/Win32Packaging.cpp +161 -0
- package/native/win32/Win32Packaging.h +56 -0
- package/native/win32/Win32RunLoopObserver.cpp +48 -0
- package/native/win32/Win32RunLoopObserver.h +42 -0
- package/native/win32/Win32ScrollView.cpp +515 -0
- package/native/win32/Win32ScrollView.h +208 -0
- package/native/win32/Win32SkiaContext.cpp +221 -0
- package/native/win32/Win32SkiaContext.h +53 -0
- package/native/win32/Win32SkiaModule.cpp +80 -0
- package/native/win32/Win32SkiaModule.h +50 -0
- package/native/win32/Win32SkiaPeer.cpp +358 -0
- package/native/win32/Win32SkiaPeer.h +66 -0
- package/native/win32/Win32SkiaUnicode.cpp +42 -0
- package/native/win32/Win32Snapshot.cpp +201 -0
- package/native/win32/Win32Snapshot.h +66 -0
- package/native/win32/Win32Strings.h +54 -0
- package/native/win32/Win32TextInput.cpp +811 -0
- package/native/win32/Win32TextInput.h +232 -0
- package/native/win32/Win32TitleBar.cpp +562 -0
- package/native/win32/Win32TitleBar.h +146 -0
- package/native/win32/Win32TitleBarLayout.cpp +46 -0
- package/native/win32/Win32TitleBarLayout.h +58 -0
- package/native/win32/Win32TouchDispatcher.cpp +443 -0
- package/native/win32/Win32TouchDispatcher.h +153 -0
- package/native/win32/Win32UiThread.h +27 -0
- package/native/win32/Win32WindowControl.cpp +329 -0
- package/native/win32/Win32WindowModule.cpp +209 -0
- package/native/win32/Win32WindowModule.h +72 -0
- package/native/win32/demo_layout_win32.cpp +232 -0
- package/native/win32/main_win32.cpp +2481 -0
- package/native/win32/mount_harness_win32.cpp +316 -0
- package/package.json +26 -0
- 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
|
+
}
|