basalt-appkit 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/runMacos.js +33 -0
- package/cli/shared.js +75 -0
- package/native/CMakeLists.txt +298 -0
- package/native/appkit/AppKitAnimationChoreographer.h +54 -0
- package/native/appkit/AppKitAnimationChoreographer.mm +125 -0
- package/native/appkit/AppKitDropTarget.h +34 -0
- package/native/appkit/AppKitDropTarget.mm +292 -0
- package/native/appkit/AppKitFocus.h +120 -0
- package/native/appkit/AppKitFocus.mm +293 -0
- package/native/appkit/AppKitImageLoader.h +80 -0
- package/native/appkit/AppKitImageLoader.mm +156 -0
- package/native/appkit/AppKitKeyEvents.h +26 -0
- package/native/appkit/AppKitKeyEvents.mm +97 -0
- package/native/appkit/AppKitMenuBar.mm +424 -0
- package/native/appkit/AppKitMountingManager.h +135 -0
- package/native/appkit/AppKitMountingManager.mm +946 -0
- package/native/appkit/AppKitRunLoopObserver.h +34 -0
- package/native/appkit/AppKitRunLoopObserver.mm +42 -0
- package/native/appkit/AppKitScrollView.h +168 -0
- package/native/appkit/AppKitScrollView.mm +550 -0
- package/native/appkit/AppKitSkiaContext.h +33 -0
- package/native/appkit/AppKitSkiaContext.mm +255 -0
- package/native/appkit/AppKitSkiaModule.h +53 -0
- package/native/appkit/AppKitSkiaModule.mm +81 -0
- package/native/appkit/AppKitSkiaPeer.h +37 -0
- package/native/appkit/AppKitSkiaPeer.mm +123 -0
- package/native/appkit/AppKitSkiaView.h +62 -0
- package/native/appkit/AppKitSnapshot.h +21 -0
- package/native/appkit/AppKitSnapshot.mm +91 -0
- package/native/appkit/AppKitTextInput.h +140 -0
- package/native/appkit/AppKitTextInput.mm +588 -0
- package/native/appkit/AppKitTextPeer.h +74 -0
- package/native/appkit/AppKitTextPeer.mm +353 -0
- package/native/appkit/AppKitTitleBar.h +90 -0
- package/native/appkit/AppKitTitleBar.mm +186 -0
- package/native/appkit/AppKitTouchDispatcher.h +138 -0
- package/native/appkit/AppKitTouchDispatcher.mm +514 -0
- package/native/appkit/AppKitWindowControl.mm +222 -0
- package/native/appkit/AppKitWindowModule.h +62 -0
- package/native/appkit/AppKitWindowModule.mm +194 -0
- package/native/appkit/ColorSchemeAppKit.mm +71 -0
- package/native/appkit/ComponentRegistryAppKit.mm +121 -0
- package/native/appkit/CoreTextLayout.h +43 -0
- package/native/appkit/CoreTextLayout.mm +252 -0
- package/native/appkit/CoreTextLayoutManager.mm +138 -0
- package/native/appkit/FontRegistryCoreText.mm +148 -0
- package/native/appkit/PlatformServicesAppKit.mm +540 -0
- package/native/appkit/RnAppKitView.h +417 -0
- package/native/appkit/RnAppKitView.mm +1605 -0
- package/native/appkit/RnTextLayout.h +38 -0
- package/native/appkit/RnTextLayout.mm +168 -0
- package/native/appkit/demo_layout_appkit.mm +93 -0
- package/native/appkit/main_appkit.mm +1860 -0
- package/native/appkit/mount_harness_appkit.mm +197 -0
- package/native/tests/test_appkit_accessibility.mm +170 -0
- package/native/tests/test_appkit_image.mm +237 -0
- package/native/tests/test_appkit_input.mm +852 -0
- package/native/tests/test_appkit_key_events.mm +150 -0
- package/native/tests/test_appkit_mounting.mm +430 -0
- package/native/tests/test_appkit_scrollbar.mm +132 -0
- package/native/tests/test_appkit_services.mm +57 -0
- package/native/tests/test_appkit_skia.mm +212 -0
- package/native/tests/test_appkit_text.mm +180 -0
- package/native/tests/test_appkit_textinput.mm +748 -0
- package/native/tests/test_appkit_titlebar.mm +107 -0
- package/native/tests/test_appkit_view.mm +237 -0
- package/package.json +26 -0
- package/react-native.config.js +22 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
// Tests for <TextInput>'s controlled-value loop.
|
|
2
|
+
//
|
|
3
|
+
// The hard part of a text field is not typing. React Native's <TextInput> is a
|
|
4
|
+
// controlled component: JavaScript owns the value, the field reports every
|
|
5
|
+
// change, JavaScript re-renders and pushes the value back down as a prop. If
|
|
6
|
+
// applying that prop looks like the user typing, the two chase each other
|
|
7
|
+
// forever; if a prop that arrives late is applied anyway, a fast typist watches
|
|
8
|
+
// characters reorder themselves.
|
|
9
|
+
//
|
|
10
|
+
// Both of those are state machines rather than AppKit, and both are tested
|
|
11
|
+
// here through real mutations, because they are only wrong in combination.
|
|
12
|
+
//
|
|
13
|
+
// Props are built through React Native's own RawProps parser rather than by
|
|
14
|
+
// assigning fields: TextInputProps' members are const and only reachable that
|
|
15
|
+
// way, which is also how the GTK suite builds them.
|
|
16
|
+
|
|
17
|
+
#include "TestHarness.h"
|
|
18
|
+
#include "EventRecorder.h"
|
|
19
|
+
|
|
20
|
+
#import "AppKitMountingManager.h"
|
|
21
|
+
#import "AppKitTextPeer.h"
|
|
22
|
+
#import "RnAppKitView.h"
|
|
23
|
+
|
|
24
|
+
#include <react/renderer/components/iostextinput/TextInputProps.h>
|
|
25
|
+
#include <react/renderer/components/textinput/TextInputEventEmitter.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 <sstream>
|
|
31
|
+
|
|
32
|
+
using facebook::react::ContextContainer;
|
|
33
|
+
using facebook::react::LayoutMetrics;
|
|
34
|
+
using facebook::react::MountingTransaction;
|
|
35
|
+
using facebook::react::PropsParserContext;
|
|
36
|
+
using facebook::react::RawProps;
|
|
37
|
+
using facebook::react::RawPropsParser;
|
|
38
|
+
using facebook::react::ShadowView;
|
|
39
|
+
using facebook::react::ShadowViewMutation;
|
|
40
|
+
using facebook::react::ShadowViewMutationList;
|
|
41
|
+
using facebook::react::SurfaceId;
|
|
42
|
+
using facebook::react::Tag;
|
|
43
|
+
using facebook::react::TextInputEventEmitter;
|
|
44
|
+
using facebook::react::TextInputProps;
|
|
45
|
+
using facebook::react::TransactionTelemetry;
|
|
46
|
+
|
|
47
|
+
namespace {
|
|
48
|
+
|
|
49
|
+
constexpr SurfaceId kSurfaceId = 1;
|
|
50
|
+
|
|
51
|
+
// One parser, prepared once: preparing walks every prop TextInputProps knows,
|
|
52
|
+
// which is not something to redo per test. RawProps has to be parsed before it
|
|
53
|
+
// can be read -- constructing one and handing it straight to a Props
|
|
54
|
+
// constructor trips an assertion inside RawProps rather than producing empty
|
|
55
|
+
// props, which is a better failure than it sounds.
|
|
56
|
+
const RawPropsParser &textInputParser() {
|
|
57
|
+
static const RawPropsParser parser = []() {
|
|
58
|
+
RawPropsParser prepared;
|
|
59
|
+
prepared.prepare<TextInputProps>();
|
|
60
|
+
return prepared;
|
|
61
|
+
}();
|
|
62
|
+
return parser;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ShadowView makeTextInput(Tag tag,
|
|
66
|
+
folly::dynamic props,
|
|
67
|
+
facebook::react::SharedEventEmitter emitter = nullptr) {
|
|
68
|
+
static const auto contextContainer = std::make_shared<const ContextContainer>();
|
|
69
|
+
PropsParserContext context{kSurfaceId, *contextContainer};
|
|
70
|
+
|
|
71
|
+
RawProps rawProps{std::move(props)};
|
|
72
|
+
rawProps.parse(textInputParser());
|
|
73
|
+
auto parsed = std::make_shared<const TextInputProps>(context, TextInputProps{}, rawProps);
|
|
74
|
+
|
|
75
|
+
LayoutMetrics metrics;
|
|
76
|
+
metrics.frame = {.origin = {.x = 0, .y = 0}, .size = {.width = 200, .height = 40}};
|
|
77
|
+
|
|
78
|
+
ShadowView view;
|
|
79
|
+
view.componentName = "TextInput";
|
|
80
|
+
view.surfaceId = kSurfaceId;
|
|
81
|
+
view.tag = tag;
|
|
82
|
+
view.props = parsed;
|
|
83
|
+
view.layoutMetrics = metrics;
|
|
84
|
+
// Null for most tests, which is what a hand-built shadow view carries and
|
|
85
|
+
// why nothing here could see an event until EventRecorder.h.
|
|
86
|
+
view.eventEmitter = std::move(emitter);
|
|
87
|
+
return view;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
void apply(basalt::AppKitMountingManager &manager, ShadowViewMutationList &&mutations) {
|
|
91
|
+
manager.applyTransaction(
|
|
92
|
+
kSurfaceId, MountingTransaction(kSurfaceId, 1, std::move(mutations), TransactionTelemetry{}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Mounts one TextInput under the surface root and hands back its view.
|
|
96
|
+
RnAppKitView *mountField(basalt::AppKitMountingManager &manager,
|
|
97
|
+
Tag tag,
|
|
98
|
+
folly::dynamic props,
|
|
99
|
+
facebook::react::SharedEventEmitter emitter = nullptr) {
|
|
100
|
+
RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
|
|
101
|
+
[root setRnFrameX:0 y:0 width:400 height:300];
|
|
102
|
+
|
|
103
|
+
ShadowViewMutationList mutations;
|
|
104
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(makeTextInput(tag, props, emitter)));
|
|
105
|
+
mutations.push_back(
|
|
106
|
+
ShadowViewMutation::InsertMutation(kSurfaceId, makeTextInput(tag, props, emitter), 0));
|
|
107
|
+
apply(manager, std::move(mutations));
|
|
108
|
+
|
|
109
|
+
return manager.viewForTag(tag);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Through the seam, because a multiline peer is an NSTextView and asking it
|
|
113
|
+
// for NSTextField's properties does not compile.
|
|
114
|
+
NSView *fieldOf(RnAppKitView *view) {
|
|
115
|
+
return view.rnEditable;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// What a person typing does, as far as the manager is concerned: the field's
|
|
119
|
+
// value changes and the delegate is told. AppKit posts that notification from
|
|
120
|
+
// NSControl and there is no field editor here to post it, so the test does what
|
|
121
|
+
// AppKit would -- which is also exactly the path a real keystroke takes.
|
|
122
|
+
void typeInto(NSView *field, NSString *text) {
|
|
123
|
+
RnPeerSetText(field, [RnPeerText(field) stringByAppendingString:text]);
|
|
124
|
+
// The notification a real NSTextField posts. Single line only: this helper
|
|
125
|
+
// stands in for AppKit, and a multiline peer posts NSTextDidChangeNotification
|
|
126
|
+
// through NSTextViewDelegate instead.
|
|
127
|
+
[((NSTextField *)field).delegate
|
|
128
|
+
controlTextDidChange:[NSNotification notificationWithName:NSControlTextDidChangeNotification
|
|
129
|
+
object:field]];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// What AppKit posts when editing ends -- the field losing focus, or the user
|
|
133
|
+
// pressing Tab. Single line only, for the same reason typeInto is.
|
|
134
|
+
void endEditing(NSView *field) {
|
|
135
|
+
[((NSTextField *)field).delegate
|
|
136
|
+
controlTextDidEndEditing:[NSNotification
|
|
137
|
+
notificationWithName:NSControlTextDidEndEditingNotification
|
|
138
|
+
object:field]];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// One Update mutation for a field, which is what every re-render produces.
|
|
142
|
+
void updateField(basalt::AppKitMountingManager &manager,
|
|
143
|
+
Tag tag,
|
|
144
|
+
folly::dynamic before,
|
|
145
|
+
folly::dynamic after) {
|
|
146
|
+
ShadowViewMutationList mutations;
|
|
147
|
+
mutations.push_back(ShadowViewMutation::UpdateMutation(
|
|
148
|
+
makeTextInput(tag, std::move(before)), makeTextInput(tag, std::move(after)), kSurfaceId));
|
|
149
|
+
apply(manager, std::move(mutations));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
} // namespace
|
|
153
|
+
|
|
154
|
+
TEST(textinput_mounts_a_real_field) {
|
|
155
|
+
@autoreleasepool {
|
|
156
|
+
basalt::AppKitMountingManager manager;
|
|
157
|
+
RnAppKitView *view = mountField(manager, 10, folly::dynamic::object("text", "hello"));
|
|
158
|
+
|
|
159
|
+
NSView *field = fieldOf(view);
|
|
160
|
+
EXPECT(field != nil);
|
|
161
|
+
EXPECT([RnPeerText(field) isEqualToString:@"hello"]);
|
|
162
|
+
// The view behind it draws the background and the border, so the field
|
|
163
|
+
// itself must paint nothing -- an NSTextField's own bezel would sit on top
|
|
164
|
+
// of whatever the style asked for.
|
|
165
|
+
EXPECT(!((NSTextField *)field).isBordered);
|
|
166
|
+
EXPECT(!((NSTextField *)field).drawsBackground);
|
|
167
|
+
|
|
168
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Applying the `text` prop must not look like the user typing. If it did, the
|
|
173
|
+
// change reported would provoke a re-render that sets it again, forever.
|
|
174
|
+
TEST(textinput_applying_a_prop_is_not_typing) {
|
|
175
|
+
@autoreleasepool {
|
|
176
|
+
basalt::AppKitMountingManager manager;
|
|
177
|
+
RnAppKitView *view = mountField(manager, 20, folly::dynamic::object("text", "one"));
|
|
178
|
+
|
|
179
|
+
ShadowViewMutationList update;
|
|
180
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
181
|
+
makeTextInput(20, folly::dynamic::object("text", "one")),
|
|
182
|
+
makeTextInput(20, folly::dynamic::object("text", "two")),
|
|
183
|
+
kSurfaceId));
|
|
184
|
+
apply(manager, std::move(update));
|
|
185
|
+
|
|
186
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"two"]);
|
|
187
|
+
|
|
188
|
+
// No emitter is attached to these hand-built shadow views, so what this
|
|
189
|
+
// pins is that the round trip did not throw or recurse -- the loop it would
|
|
190
|
+
// enter has no exit, so a failure here is a hang rather than a wrong value.
|
|
191
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The field is placed inside the content inset, so paddingHorizontal on a field
|
|
196
|
+
// means what it means on a <View>. Without this the text sits flush against the
|
|
197
|
+
// border, ignoring the style.
|
|
198
|
+
// An uncontrolled field keeps what was typed into it.
|
|
199
|
+
//
|
|
200
|
+
// React Native re-sends `mostRecentEventCount` on every change, which is an
|
|
201
|
+
// Update mutation on its own, and `text` for an uncontrolled field is the empty
|
|
202
|
+
// string forever -- `TextInput.js` sends `text={value ?? defaultValue}` and an
|
|
203
|
+
// uncontrolled field with no default sends undefined. So a manager that applies
|
|
204
|
+
// the prop whenever it differs from the field wipes it on the user's second
|
|
205
|
+
// keystroke. Applying it only when the *prop* changes is what makes this work;
|
|
206
|
+
// found on Windows in phase 46, and this platform had the same bug.
|
|
207
|
+
TEST(textinput_keeps_what_was_typed_into_an_uncontrolled_field) {
|
|
208
|
+
@autoreleasepool {
|
|
209
|
+
basalt::AppKitMountingManager manager;
|
|
210
|
+
RnAppKitView *view = mountField(manager, 30, folly::dynamic::object("text", ""));
|
|
211
|
+
|
|
212
|
+
typeInto(fieldOf(view), @"abc");
|
|
213
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"abc"]);
|
|
214
|
+
|
|
215
|
+
// The re-render an uncontrolled field produces: no text, a bumped count.
|
|
216
|
+
updateField(manager,
|
|
217
|
+
30,
|
|
218
|
+
folly::dynamic::object("text", ""),
|
|
219
|
+
folly::dynamic::object("text", "")("mostRecentEventCount", 1));
|
|
220
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"abc"]);
|
|
221
|
+
|
|
222
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// The same staleness rule the commands already follow, applied to the prop. A
|
|
227
|
+
// value React rendered before the user's latest keystroke must not be applied,
|
|
228
|
+
// or a fast typist watches characters reorder themselves -- and it must not be
|
|
229
|
+
// *forgotten* either, or the value is lost once JavaScript catches up.
|
|
230
|
+
TEST(textinput_drops_a_text_prop_older_than_what_was_typed) {
|
|
231
|
+
@autoreleasepool {
|
|
232
|
+
basalt::AppKitMountingManager manager;
|
|
233
|
+
RnAppKitView *view = mountField(manager, 31, folly::dynamic::object("text", ""));
|
|
234
|
+
|
|
235
|
+
typeInto(fieldOf(view), @"fast");
|
|
236
|
+
|
|
237
|
+
updateField(manager,
|
|
238
|
+
31,
|
|
239
|
+
folly::dynamic::object("text", ""),
|
|
240
|
+
folly::dynamic::object("text", "F")("mostRecentEventCount", 0));
|
|
241
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"fast"]);
|
|
242
|
+
|
|
243
|
+
updateField(manager,
|
|
244
|
+
31,
|
|
245
|
+
folly::dynamic::object("text", ""),
|
|
246
|
+
folly::dynamic::object("text", "F")("mostRecentEventCount", 1));
|
|
247
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"F"]);
|
|
248
|
+
|
|
249
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// A `selection` prop on a field that has no focus.
|
|
254
|
+
//
|
|
255
|
+
// On AppKit a selection lives on the *field editor* -- a shared NSTextView the
|
|
256
|
+
// window lends to whichever field is first responder -- so an unfocused field
|
|
257
|
+
// has nowhere to put one. Props routinely arrive before focus does, so the
|
|
258
|
+
// only correct behaviour is to leave the text alone and not reach through a
|
|
259
|
+
// nil editor, which is what this pins.
|
|
260
|
+
//
|
|
261
|
+
// The other half, that a selection actually lands, needs a field editor and so
|
|
262
|
+
// needs a window, which this suite deliberately does not have. GTK's
|
|
263
|
+
// test_textinput.cpp covers that half on a host where the selection belongs to
|
|
264
|
+
// the widget itself.
|
|
265
|
+
TEST(textinput_a_selection_prop_without_focus_is_harmless) {
|
|
266
|
+
@autoreleasepool {
|
|
267
|
+
basalt::AppKitMountingManager manager;
|
|
268
|
+
RnAppKitView *view = mountField(manager, 20, folly::dynamic::object("text", "abcdef"));
|
|
269
|
+
|
|
270
|
+
ShadowViewMutationList update;
|
|
271
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
272
|
+
makeTextInput(20, folly::dynamic::object("text", "abcdef")),
|
|
273
|
+
makeTextInput(20,
|
|
274
|
+
folly::dynamic::object("text", "abcdef")(
|
|
275
|
+
"selection", folly::dynamic::object("start", 2)("end", 5))),
|
|
276
|
+
kSurfaceId));
|
|
277
|
+
apply(manager, std::move(update));
|
|
278
|
+
|
|
279
|
+
EXPECT(RnPeerEditor(fieldOf(view)) == nil);
|
|
280
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"abcdef"]);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
TEST(textinput_sits_inside_the_content_inset) {
|
|
285
|
+
@autoreleasepool {
|
|
286
|
+
basalt::AppKitMountingManager manager;
|
|
287
|
+
RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
|
|
288
|
+
[root setRnFrameX:0 y:0 width:400 height:300];
|
|
289
|
+
|
|
290
|
+
ShadowView shadowView = makeTextInput(30, folly::dynamic::object("text", ""));
|
|
291
|
+
shadowView.layoutMetrics.contentInsets = {.left = 12, .top = 4, .right = 12, .bottom = 4};
|
|
292
|
+
|
|
293
|
+
ShadowViewMutationList mutations;
|
|
294
|
+
mutations.push_back(ShadowViewMutation::CreateMutation(shadowView));
|
|
295
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(kSurfaceId, shadowView, 0));
|
|
296
|
+
apply(manager, std::move(mutations));
|
|
297
|
+
|
|
298
|
+
NSView *field = fieldOf(manager.viewForTag(30));
|
|
299
|
+
EXPECT(field != nil);
|
|
300
|
+
EXPECT_NEAR(field.frame.origin.x, 12.0, 0.001);
|
|
301
|
+
EXPECT_NEAR(field.frame.origin.y, 4.0, 0.001);
|
|
302
|
+
// 200 wide less 12 either side; 40 tall less 4 top and bottom.
|
|
303
|
+
EXPECT_NEAR(field.frame.size.width, 176.0, 0.001);
|
|
304
|
+
EXPECT_NEAR(field.frame.size.height, 32.0, 0.001);
|
|
305
|
+
|
|
306
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// secureTextEntry is a different *class* on AppKit, not a property, so turning
|
|
311
|
+
// it on has to rebuild the field -- and the text has to survive that.
|
|
312
|
+
TEST(textinput_secure_entry_rebuilds_the_field) {
|
|
313
|
+
@autoreleasepool {
|
|
314
|
+
basalt::AppKitMountingManager manager;
|
|
315
|
+
RnAppKitView *view = mountField(manager, 40, folly::dynamic::object("text", "shh"));
|
|
316
|
+
|
|
317
|
+
EXPECT(![fieldOf(view) isKindOfClass:[NSSecureTextField class]]);
|
|
318
|
+
|
|
319
|
+
ShadowViewMutationList update;
|
|
320
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
321
|
+
makeTextInput(40, folly::dynamic::object("text", "shh")),
|
|
322
|
+
makeTextInput(40, folly::dynamic::object("text", "shh")("secureTextEntry", true)),
|
|
323
|
+
kSurfaceId));
|
|
324
|
+
apply(manager, std::move(update));
|
|
325
|
+
|
|
326
|
+
EXPECT([fieldOf(view) isKindOfClass:[NSSecureTextField class]]);
|
|
327
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"shh"]);
|
|
328
|
+
|
|
329
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// `editable: false` and `readOnly: true` are the same request spelled two ways,
|
|
334
|
+
// and React Native honours both.
|
|
335
|
+
TEST(textinput_honours_both_spellings_of_read_only) {
|
|
336
|
+
@autoreleasepool {
|
|
337
|
+
basalt::AppKitMountingManager manager;
|
|
338
|
+
|
|
339
|
+
RnAppKitView *plain = mountField(manager, 50, folly::dynamic::object("text", ""));
|
|
340
|
+
EXPECT(((NSTextField *)fieldOf(plain)).isEditable);
|
|
341
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
342
|
+
|
|
343
|
+
basalt::AppKitMountingManager second;
|
|
344
|
+
RnAppKitView *locked =
|
|
345
|
+
mountField(second, 51, folly::dynamic::object("text", "")("editable", false));
|
|
346
|
+
EXPECT(!((NSTextField *)fieldOf(locked)).isEditable);
|
|
347
|
+
second.destroySurfaceRoot(kSurfaceId);
|
|
348
|
+
|
|
349
|
+
basalt::AppKitMountingManager third;
|
|
350
|
+
RnAppKitView *readOnly =
|
|
351
|
+
mountField(third, 52, folly::dynamic::object("text", "")("readOnly", true));
|
|
352
|
+
EXPECT(!((NSTextField *)fieldOf(readOnly)).isEditable);
|
|
353
|
+
third.destroySurfaceRoot(kSurfaceId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// The field's contents have to show up in the tree dump: they live in the
|
|
358
|
+
// NSTextField, not in anything the view draws, so they would otherwise be
|
|
359
|
+
// invisible to every test that reads the tree -- and to the cross-platform
|
|
360
|
+
// diff, which is where a difference in them would matter most.
|
|
361
|
+
TEST(textinput_contents_are_reported_in_the_tree) {
|
|
362
|
+
@autoreleasepool {
|
|
363
|
+
basalt::AppKitMountingManager manager;
|
|
364
|
+
RnAppKitView *root = manager.createSurfaceRoot(kSurfaceId);
|
|
365
|
+
[root setRnFrameX:0 y:0 width:400 height:300];
|
|
366
|
+
|
|
367
|
+
ShadowViewMutationList mutations;
|
|
368
|
+
mutations.push_back(
|
|
369
|
+
ShadowViewMutation::CreateMutation(makeTextInput(60, folly::dynamic::object("text", "abc"))));
|
|
370
|
+
mutations.push_back(ShadowViewMutation::InsertMutation(
|
|
371
|
+
kSurfaceId, makeTextInput(60, folly::dynamic::object("text", "abc")), 0));
|
|
372
|
+
apply(manager, std::move(mutations));
|
|
373
|
+
|
|
374
|
+
const std::string described = [root describeTree].UTF8String;
|
|
375
|
+
EXPECT(described.find("editable=\"abc\"") != std::string::npos);
|
|
376
|
+
|
|
377
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// A command for a field that was never mounted, and one that is not ours. Both
|
|
382
|
+
// have to be survivable: a focus arriving between a Remove and its Delete is
|
|
383
|
+
// exactly this shape.
|
|
384
|
+
// --- multiline ---------------------------------------------------------------
|
|
385
|
+
//
|
|
386
|
+
// Deliberately the same four questions test_textinput.cpp asks of GTK, because
|
|
387
|
+
// both hosts now reach their peer through a seam and what these really check
|
|
388
|
+
// is whether the two seams agree.
|
|
389
|
+
|
|
390
|
+
TEST(textinput_multiline_builds_a_text_view) {
|
|
391
|
+
@autoreleasepool {
|
|
392
|
+
basalt::AppKitMountingManager manager;
|
|
393
|
+
RnAppKitView *view =
|
|
394
|
+
mountField(manager, 60, folly::dynamic::object("text", "hello")("multiline", true));
|
|
395
|
+
|
|
396
|
+
NSView *peer = fieldOf(view);
|
|
397
|
+
EXPECT(peer != nil);
|
|
398
|
+
EXPECT(RnPeerIsMultiline(peer));
|
|
399
|
+
EXPECT([peer isKindOfClass:[NSTextView class]]);
|
|
400
|
+
|
|
401
|
+
// And a plain field is still an NSTextField, which is the half that must
|
|
402
|
+
// not have moved.
|
|
403
|
+
RnAppKitView *single = mountField(manager, 61, folly::dynamic::object("text", "hello"));
|
|
404
|
+
EXPECT(!RnPeerIsMultiline(fieldOf(single)));
|
|
405
|
+
EXPECT([fieldOf(single) isKindOfClass:[NSTextField class]]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
TEST(textinput_multiline_text_round_trips) {
|
|
410
|
+
@autoreleasepool {
|
|
411
|
+
basalt::AppKitMountingManager manager;
|
|
412
|
+
RnAppKitView *view =
|
|
413
|
+
mountField(manager, 62, folly::dynamic::object("text", "one")("multiline", true));
|
|
414
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"one"]);
|
|
415
|
+
|
|
416
|
+
// Newlines are the point, and are what a single-line field would refuse.
|
|
417
|
+
ShadowViewMutationList update;
|
|
418
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
419
|
+
makeTextInput(62, folly::dynamic::object("text", "one")("multiline", true)),
|
|
420
|
+
makeTextInput(62, folly::dynamic::object("text", "one\ntwo")("multiline", true)),
|
|
421
|
+
kSurfaceId));
|
|
422
|
+
apply(manager, std::move(update));
|
|
423
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"one\ntwo"]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
TEST(textinput_multiline_owns_its_selection) {
|
|
428
|
+
@autoreleasepool {
|
|
429
|
+
basalt::AppKitMountingManager manager;
|
|
430
|
+
RnAppKitView *view =
|
|
431
|
+
mountField(manager, 63, folly::dynamic::object("text", "abcdef")("multiline", true));
|
|
432
|
+
|
|
433
|
+
// The one place multiline is simpler: an NSTextView *is* the editor, so it
|
|
434
|
+
// has a selection whether or not anything has focus -- where an unfocused
|
|
435
|
+
// NSTextField has no field editor and so none at all.
|
|
436
|
+
NSView *peer = fieldOf(view);
|
|
437
|
+
EXPECT(RnPeerEditor(peer) != nil);
|
|
438
|
+
|
|
439
|
+
RnPeerSetSelection(peer, NSMakeRange(1, 3));
|
|
440
|
+
const NSRange selected = RnPeerSelection(peer);
|
|
441
|
+
EXPECT_EQ((long)selected.location, 1L);
|
|
442
|
+
EXPECT_EQ((long)selected.length, 3L);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
TEST(textinput_switching_multiline_rebuilds_the_peer) {
|
|
447
|
+
@autoreleasepool {
|
|
448
|
+
basalt::AppKitMountingManager manager;
|
|
449
|
+
RnAppKitView *view = mountField(manager, 64, folly::dynamic::object("text", "hello"));
|
|
450
|
+
EXPECT(!RnPeerIsMultiline(fieldOf(view)));
|
|
451
|
+
|
|
452
|
+
// Not a property that can be flipped, so the widget is replaced -- and the
|
|
453
|
+
// text has to survive that, because React did not ask for it to be cleared.
|
|
454
|
+
ShadowViewMutationList update;
|
|
455
|
+
update.push_back(ShadowViewMutation::UpdateMutation(
|
|
456
|
+
makeTextInput(64, folly::dynamic::object("text", "hello")),
|
|
457
|
+
makeTextInput(64, folly::dynamic::object("text", "hello")("multiline", true)),
|
|
458
|
+
kSurfaceId));
|
|
459
|
+
apply(manager, std::move(update));
|
|
460
|
+
|
|
461
|
+
EXPECT(RnPeerIsMultiline(fieldOf(view)));
|
|
462
|
+
EXPECT([RnPeerText(fieldOf(view)) isEqualToString:@"hello"]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// A single-line field clips rather than wrapping, which is what it does on
|
|
467
|
+
// every other platform -- it scrolls horizontally instead. An NSTextField
|
|
468
|
+
// wraps by default, and that was invisible here until a field held more than
|
|
469
|
+
// it could show.
|
|
470
|
+
TEST(textinput_single_line_does_not_wrap) {
|
|
471
|
+
@autoreleasepool {
|
|
472
|
+
basalt::AppKitMountingManager manager;
|
|
473
|
+
RnAppKitView *view = mountField(manager, 65, folly::dynamic::object("text", "hello"));
|
|
474
|
+
NSTextField *field = (NSTextField *)fieldOf(view);
|
|
475
|
+
EXPECT(field.usesSingleLineMode);
|
|
476
|
+
EXPECT(!field.cell.wraps);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// maxLength, which AppKit offers nothing for on either peer.
|
|
481
|
+
TEST(textinput_max_length_is_enforced_on_both_peers) {
|
|
482
|
+
@autoreleasepool {
|
|
483
|
+
basalt::AppKitMountingManager manager;
|
|
484
|
+
|
|
485
|
+
// A single-line field takes a formatter, which is what AppKit offers.
|
|
486
|
+
RnAppKitView *single =
|
|
487
|
+
mountField(manager, 70, folly::dynamic::object("text", "")("maxLength", 5));
|
|
488
|
+
NSTextField *field = (NSTextField *)fieldOf(single);
|
|
489
|
+
EXPECT(field.formatter != nil);
|
|
490
|
+
NSString *replacement = nil;
|
|
491
|
+
NSString *error = nil;
|
|
492
|
+
EXPECT([field.formatter isPartialStringValid:@"abcde"
|
|
493
|
+
newEditingString:&replacement
|
|
494
|
+
errorDescription:&error]);
|
|
495
|
+
EXPECT(![field.formatter isPartialStringValid:@"abcdef"
|
|
496
|
+
newEditingString:&replacement
|
|
497
|
+
errorDescription:&error]);
|
|
498
|
+
|
|
499
|
+
// A multiline one has no formatter, so the limit is asked about instead --
|
|
500
|
+
// refused whole rather than truncated.
|
|
501
|
+
RnAppKitView *multi = mountField(
|
|
502
|
+
manager, 71, folly::dynamic::object("text", "abcd")("multiline", true)("maxLength", 5));
|
|
503
|
+
NSView *peer = fieldOf(multi);
|
|
504
|
+
EXPECT(RnPeerAllowsChange(peer, NSMakeRange(4, 0), @"e"));
|
|
505
|
+
EXPECT(!RnPeerAllowsChange(peer, NSMakeRange(4, 0), @"efgh"));
|
|
506
|
+
// Replacing a selection makes room, so length alone is not the question.
|
|
507
|
+
EXPECT(RnPeerAllowsChange(peer, NSMakeRange(0, 4), @"vwxyz"));
|
|
508
|
+
|
|
509
|
+
// And no limit means no limit.
|
|
510
|
+
RnAppKitView *free = mountField(manager, 72, folly::dynamic::object("text", "")("multiline", true));
|
|
511
|
+
EXPECT(RnPeerAllowsChange(fieldOf(free), NSMakeRange(0, 0), @"as long as you like"));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// A placeholder on the peer that has no property for it.
|
|
516
|
+
TEST(textinput_multiline_takes_a_placeholder) {
|
|
517
|
+
@autoreleasepool {
|
|
518
|
+
basalt::AppKitMountingManager manager;
|
|
519
|
+
RnAppKitView *view = mountField(
|
|
520
|
+
manager, 73,
|
|
521
|
+
folly::dynamic::object("text", "")("multiline", true)("placeholder", "type here"));
|
|
522
|
+
|
|
523
|
+
NSAttributedString *placeholder = RnPeerPlaceholder(fieldOf(view));
|
|
524
|
+
EXPECT(placeholder != nil);
|
|
525
|
+
EXPECT([placeholder.string isEqualToString:@"type here"]);
|
|
526
|
+
|
|
527
|
+
// And the single-line peer still takes its own, through the property
|
|
528
|
+
// AppKit gives it.
|
|
529
|
+
RnAppKitView *single =
|
|
530
|
+
mountField(manager, 74, folly::dynamic::object("text", "")("placeholder", "type here"));
|
|
531
|
+
EXPECT([RnPeerPlaceholder(fieldOf(single)).string isEqualToString:@"type here"]);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
TEST(textinput_commands_survive_an_unknown_tag) {
|
|
536
|
+
@autoreleasepool {
|
|
537
|
+
basalt::AppKitMountingManager manager;
|
|
538
|
+
mountField(manager, 70, folly::dynamic::object("text", ""));
|
|
539
|
+
|
|
540
|
+
manager.applyCommand(9999, "focus", folly::dynamic::array());
|
|
541
|
+
manager.applyCommand(70, "somethingElse", folly::dynamic::array());
|
|
542
|
+
manager.applyCommand(70, "blur", folly::dynamic::array());
|
|
543
|
+
|
|
544
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// --- What React actually hears ---------------------------------------------
|
|
549
|
+
//
|
|
550
|
+
// Everything above pins the field's own state. These pin the events, which
|
|
551
|
+
// nothing below the end-to-end suite could see until EventRecorder.h -- see
|
|
552
|
+
// that header for why a stub emitter cannot do this and a real dispatcher can.
|
|
553
|
+
|
|
554
|
+
TEST(textinput_change_reaches_the_emitter) {
|
|
555
|
+
@autoreleasepool {
|
|
556
|
+
basalt::testing::EventRecorder recorder;
|
|
557
|
+
basalt::AppKitMountingManager manager;
|
|
558
|
+
RnAppKitView *view = mountField(manager,
|
|
559
|
+
80,
|
|
560
|
+
folly::dynamic::object("text", ""),
|
|
561
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
562
|
+
|
|
563
|
+
typeInto(fieldOf(view), @"a");
|
|
564
|
+
|
|
565
|
+
const auto seen = recorder.seen();
|
|
566
|
+
EXPECT_EQ(seen.size(), 1u);
|
|
567
|
+
EXPECT_EQ(seen.empty() ? std::string{} : seen[0], std::string{"topChange"});
|
|
568
|
+
|
|
569
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// A genuine ordering assertion, and the first one in either suite: one call
|
|
574
|
+
// site emits two events, and React Native's contract is blur before
|
|
575
|
+
// endEditing. Nothing checked that they both fired, never mind in which
|
|
576
|
+
// order.
|
|
577
|
+
TEST(textinput_blur_reports_blur_before_end_editing) {
|
|
578
|
+
@autoreleasepool {
|
|
579
|
+
basalt::testing::EventRecorder recorder;
|
|
580
|
+
basalt::AppKitMountingManager manager;
|
|
581
|
+
RnAppKitView *view = mountField(manager,
|
|
582
|
+
81,
|
|
583
|
+
folly::dynamic::object("text", "hello"),
|
|
584
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
585
|
+
|
|
586
|
+
endEditing(fieldOf(view));
|
|
587
|
+
|
|
588
|
+
const auto seen = recorder.seen();
|
|
589
|
+
EXPECT_EQ(seen.size(), 2u);
|
|
590
|
+
if (seen.size() == 2) {
|
|
591
|
+
EXPECT_EQ(seen[0], std::string{"topBlur"});
|
|
592
|
+
EXPECT_EQ(seen[1], std::string{"topEndEditing"});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// A field nobody typed into reports nothing. Worth pinning because the
|
|
600
|
+
// recorder would be just as quiet if it were wired up wrong, and every
|
|
601
|
+
// assertion above rests on it hearing what it should.
|
|
602
|
+
TEST(textinput_mounting_alone_reports_nothing) {
|
|
603
|
+
@autoreleasepool {
|
|
604
|
+
basalt::testing::EventRecorder recorder;
|
|
605
|
+
basalt::AppKitMountingManager manager;
|
|
606
|
+
mountField(manager,
|
|
607
|
+
82,
|
|
608
|
+
folly::dynamic::object("text", "hello"),
|
|
609
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
610
|
+
|
|
611
|
+
EXPECT(recorder.seen().empty());
|
|
612
|
+
|
|
613
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// --- Focus and keys, through a real window ----------------------------------
|
|
618
|
+
//
|
|
619
|
+
// These need a window, which the rest of this file does not: focus is
|
|
620
|
+
// AppKit's to give, and the manager watches keys with a local NSEvent monitor
|
|
621
|
+
// rather than through the delegate -- deliberately, so that it sees the key
|
|
622
|
+
// on its way to the responder chain, before the edit and therefore before
|
|
623
|
+
// onChange.
|
|
624
|
+
//
|
|
625
|
+
// The window is never ordered front, like the ones in
|
|
626
|
+
// test_appkit_titlebar.mm, and that is also the limit of what can be pinned
|
|
627
|
+
// here. NSApp routes a key to the *key* window, and a window belonging to a
|
|
628
|
+
// process that is not active cannot become key -- `makeKeyWindow` on one
|
|
629
|
+
// leaves `isKeyWindow` false. So the monitor can be driven and the edit it
|
|
630
|
+
// precedes cannot, and onKeyPress-before-onChange stays the end-to-end
|
|
631
|
+
// suite's to check rather than being arranged here and called a test.
|
|
632
|
+
//
|
|
633
|
+
// Payload values are out of reach for the same reason everywhere else: which
|
|
634
|
+
// key it was lives in a jsi::Object. What is checked is that a key press is
|
|
635
|
+
// reported, and for which keys it is not.
|
|
636
|
+
|
|
637
|
+
namespace {
|
|
638
|
+
|
|
639
|
+
// A field that is first responder in a window nobody can see.
|
|
640
|
+
NSWindow *windowAround(basalt::AppKitMountingManager &manager, NSView *field) {
|
|
641
|
+
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 400, 300)
|
|
642
|
+
styleMask:NSWindowStyleMaskTitled
|
|
643
|
+
backing:NSBackingStoreBuffered
|
|
644
|
+
defer:NO];
|
|
645
|
+
[window.contentView addSubview:manager.getSurfaceRoot(kSurfaceId)];
|
|
646
|
+
[window makeFirstResponder:field];
|
|
647
|
+
return window;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// One key, through NSApp -- which is what invokes a local event monitor.
|
|
651
|
+
// Posted and pulled rather than handed straight to sendEvent: so that it
|
|
652
|
+
// travels the queue a real keystroke travels.
|
|
653
|
+
void pressKey(NSWindow *window, unsigned short keyCode, NSString *characters) {
|
|
654
|
+
NSEvent *key = [NSEvent keyEventWithType:NSEventTypeKeyDown
|
|
655
|
+
location:NSZeroPoint
|
|
656
|
+
modifierFlags:0
|
|
657
|
+
timestamp:0
|
|
658
|
+
windowNumber:window.windowNumber
|
|
659
|
+
context:nil
|
|
660
|
+
characters:characters
|
|
661
|
+
charactersIgnoringModifiers:characters
|
|
662
|
+
isARepeat:NO
|
|
663
|
+
keyCode:keyCode];
|
|
664
|
+
[NSApp postEvent:key atStart:YES];
|
|
665
|
+
NSEvent *pulled = [NSApp nextEventMatchingMask:NSEventMaskAny
|
|
666
|
+
untilDate:[NSDate distantPast]
|
|
667
|
+
inMode:NSDefaultRunLoopMode
|
|
668
|
+
dequeue:YES];
|
|
669
|
+
if (pulled != nil) {
|
|
670
|
+
[NSApp sendEvent:pulled];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
} // namespace
|
|
675
|
+
|
|
676
|
+
// Focus is AppKit's to give and React's to hear about. Nothing checked that
|
|
677
|
+
// it was passed on.
|
|
678
|
+
TEST(textinput_focus_reaches_the_emitter) {
|
|
679
|
+
@autoreleasepool {
|
|
680
|
+
[NSApplication sharedApplication];
|
|
681
|
+
|
|
682
|
+
basalt::testing::EventRecorder recorder;
|
|
683
|
+
basalt::AppKitMountingManager manager;
|
|
684
|
+
RnAppKitView *view = mountField(manager,
|
|
685
|
+
83,
|
|
686
|
+
folly::dynamic::object("text", ""),
|
|
687
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
688
|
+
|
|
689
|
+
NSWindow *window = windowAround(manager, fieldOf(view));
|
|
690
|
+
EXPECT(window.firstResponder != window);
|
|
691
|
+
|
|
692
|
+
const auto seen = recorder.seen();
|
|
693
|
+
EXPECT_EQ(seen.size(), 1u);
|
|
694
|
+
EXPECT_EQ(seen.empty() ? std::string{} : seen[0], std::string{"topFocus"});
|
|
695
|
+
|
|
696
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
TEST(textinput_reports_a_key_press_from_the_event_queue) {
|
|
701
|
+
@autoreleasepool {
|
|
702
|
+
[NSApplication sharedApplication];
|
|
703
|
+
|
|
704
|
+
basalt::testing::EventRecorder recorder;
|
|
705
|
+
basalt::AppKitMountingManager manager;
|
|
706
|
+
RnAppKitView *view = mountField(manager,
|
|
707
|
+
84,
|
|
708
|
+
folly::dynamic::object("text", ""),
|
|
709
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
710
|
+
|
|
711
|
+
NSWindow *window = windowAround(manager, fieldOf(view));
|
|
712
|
+
recorder.clear(); // The focus above, which the test before this one owns.
|
|
713
|
+
|
|
714
|
+
pressKey(window, 0, @"a");
|
|
715
|
+
|
|
716
|
+
const auto seen = recorder.seen();
|
|
717
|
+
EXPECT_EQ(seen.size(), 1u);
|
|
718
|
+
EXPECT_EQ(seen.empty() ? std::string{} : seen[0], std::string{"topKeyPress"});
|
|
719
|
+
|
|
720
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// A key that types nothing reports nothing, which is what iOS does. The
|
|
725
|
+
// arrows and the function keys live in the Unicode private use area, and the
|
|
726
|
+
// manager filters on that -- a filter nothing exercised.
|
|
727
|
+
TEST(textinput_says_nothing_about_a_key_that_types_nothing) {
|
|
728
|
+
@autoreleasepool {
|
|
729
|
+
[NSApplication sharedApplication];
|
|
730
|
+
|
|
731
|
+
basalt::testing::EventRecorder recorder;
|
|
732
|
+
basalt::AppKitMountingManager manager;
|
|
733
|
+
RnAppKitView *view = mountField(manager,
|
|
734
|
+
85,
|
|
735
|
+
folly::dynamic::object("text", ""),
|
|
736
|
+
recorder.emitter<TextInputEventEmitter>());
|
|
737
|
+
|
|
738
|
+
NSWindow *window = windowAround(manager, fieldOf(view));
|
|
739
|
+
recorder.clear();
|
|
740
|
+
|
|
741
|
+
// Left arrow: keyCode 123, and NSLeftArrowFunctionKey as its character.
|
|
742
|
+
pressKey(window, 123, [NSString stringWithFormat:@"%C", (unichar)NSLeftArrowFunctionKey]);
|
|
743
|
+
|
|
744
|
+
EXPECT(recorder.seen().empty());
|
|
745
|
+
|
|
746
|
+
manager.destroySurfaceRoot(kSurfaceId);
|
|
747
|
+
}
|
|
748
|
+
}
|