basalt-appkit 0.0.0-rc5 → 0.1.0
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/native/CMakeLists.txt +16 -2
- package/native/appkit/AppKitAppWindow.h +24 -0
- package/native/appkit/AppKitImageLoader.h +48 -1
- package/native/appkit/AppKitImageLoader.mm +228 -10
- package/native/appkit/AppKitMenuBar.mm +58 -0
- package/native/appkit/AppKitMountingManager.h +34 -0
- package/native/appkit/AppKitMountingManager.mm +576 -9
- package/native/appkit/AppKitScrollView.h +48 -0
- package/native/appkit/AppKitScrollView.mm +210 -0
- package/native/appkit/AppKitTextInput.h +38 -1
- package/native/appkit/AppKitTextInput.mm +235 -8
- package/native/appkit/AppKitTextPeer.h +60 -0
- package/native/appkit/AppKitTextPeer.mm +307 -0
- package/native/appkit/AppKitTouchDispatcher.h +56 -0
- package/native/appkit/AppKitTouchDispatcher.mm +140 -0
- package/native/appkit/AppKitWindowControl.mm +38 -3
- package/native/appkit/ComponentRegistryAppKit.mm +20 -0
- package/native/appkit/CoreTextLayout.h +64 -2
- package/native/appkit/CoreTextLayout.mm +431 -71
- package/native/appkit/CoreTextLayoutManager.mm +57 -6
- package/native/appkit/FontRegistryCoreText.mm +6 -5
- package/native/appkit/PlatformServicesAppKit.mm +101 -20
- package/native/appkit/RnAppKitView.h +492 -0
- package/native/appkit/RnAppKitView.mm +2266 -23
- package/native/appkit/RnTextLayout.h +106 -0
- package/native/appkit/RnTextLayout.mm +321 -2
- package/native/appkit/main_appkit.mm +174 -3
- package/native/tests/test_appkit_accessibility.mm +289 -0
- package/native/tests/test_appkit_borders.mm +975 -0
- package/native/tests/test_appkit_fonts.mm +203 -0
- package/native/tests/test_appkit_image.mm +392 -17
- package/native/tests/test_appkit_input.mm +209 -0
- package/native/tests/test_appkit_mounting.mm +360 -0
- package/native/tests/test_appkit_scrollbar.mm +125 -0
- package/native/tests/test_appkit_scrollview.mm +371 -0
- package/native/tests/test_appkit_selection.mm +349 -0
- package/native/tests/test_appkit_services.mm +51 -1
- package/native/tests/test_appkit_text.mm +1628 -0
- package/native/tests/test_appkit_textinput.mm +819 -0
- package/native/tests/test_appkit_titlebar.mm +43 -0
- package/native/tests/test_appkit_view.mm +609 -0
- package/package.json +4 -3
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// Selecting a paragraph's text with a pointer, on AppKit.
|
|
2
|
+
//
|
|
3
|
+
// The same three questions the GTK suite asks, over the same claims, because
|
|
4
|
+
// the point of asking twice is that the two hosts agree: where a point lands in
|
|
5
|
+
// the text, what the highlight looks like, and what a press and a drag do. The
|
|
6
|
+
// state machine behind the third is core/TextSelection.h and has its own tests.
|
|
7
|
+
//
|
|
8
|
+
// The pixels matter here more than usual. A selection is a wash under the
|
|
9
|
+
// glyphs, so no tree dump can show it: a highlight drawn at the wrong offset,
|
|
10
|
+
// or not drawn at all, looks identical to every assertion that is not about
|
|
11
|
+
// colour at a point.
|
|
12
|
+
|
|
13
|
+
#include "TestHarness.h"
|
|
14
|
+
|
|
15
|
+
#include "AppKitMountingManager.h"
|
|
16
|
+
#include "AppKitTouchDispatcher.h"
|
|
17
|
+
#include "CoreTextLayout.h"
|
|
18
|
+
#include "PlatformServices.h"
|
|
19
|
+
#include "RnAppKitView.h"
|
|
20
|
+
#include "RnTextLayout.h"
|
|
21
|
+
#include "TextSelection.h"
|
|
22
|
+
|
|
23
|
+
#include <react/renderer/attributedstring/AttributedString.h>
|
|
24
|
+
#include <react/renderer/attributedstring/ParagraphAttributes.h>
|
|
25
|
+
|
|
26
|
+
#include <sstream>
|
|
27
|
+
#include <string>
|
|
28
|
+
|
|
29
|
+
namespace {
|
|
30
|
+
|
|
31
|
+
RnTextLayout *paragraphLayout(const char *text, float width) {
|
|
32
|
+
facebook::react::TextAttributes attributes;
|
|
33
|
+
attributes.fontSize = 24.0F;
|
|
34
|
+
facebook::react::AttributedString::Fragment fragment;
|
|
35
|
+
fragment.string = text;
|
|
36
|
+
fragment.textAttributes = attributes;
|
|
37
|
+
facebook::react::AttributedString string;
|
|
38
|
+
string.appendFragment(std::move(fragment));
|
|
39
|
+
(void)width;
|
|
40
|
+
return basalt::buildTextLayout(string, facebook::react::ParagraphAttributes{});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// A view with a paragraph in it, sized and selectable.
|
|
44
|
+
RnAppKitView *paragraphView(const char *text, CGFloat width, CGFloat height, BOOL selectable) {
|
|
45
|
+
RnAppKitView *view = [RnAppKitView viewWithTag:7];
|
|
46
|
+
view.frame = NSMakeRect(0, 0, width, height);
|
|
47
|
+
[view setRnTextLayout:paragraphLayout(text, static_cast<float>(width))];
|
|
48
|
+
[view setRnTextSelectable:selectable];
|
|
49
|
+
return view;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// How many pixels in a band are the selection's blue, which is a wash over
|
|
53
|
+
// white: the blue channel ends up well clear of the other two.
|
|
54
|
+
//
|
|
55
|
+
// The paragraph alone rather than a whole view: the highlight is drawn by the
|
|
56
|
+
// layout, and a view's own drawRect: wants a layer-backed window this does not
|
|
57
|
+
// need.
|
|
58
|
+
int highlightPixels(RnTextLayout *layout, CGSize size, int fromX, int toX) {
|
|
59
|
+
CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
60
|
+
CGContextRef context = CGBitmapContextCreate(nullptr, (size_t)size.width, (size_t)size.height, 8,
|
|
61
|
+
0, space, kCGImageAlphaPremultipliedLast);
|
|
62
|
+
CGColorSpaceRelease(space);
|
|
63
|
+
CGContextSetRGBFillColor(context, 1, 1, 1, 1);
|
|
64
|
+
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
|
|
65
|
+
[layout drawInContext:context size:size];
|
|
66
|
+
|
|
67
|
+
auto *pixels = static_cast<unsigned char *>(CGBitmapContextGetData(context));
|
|
68
|
+
const size_t stride = CGBitmapContextGetBytesPerRow(context);
|
|
69
|
+
int count = 0;
|
|
70
|
+
for (int x = fromX; x < toX; x++) {
|
|
71
|
+
for (int y = 0; y < (int)size.height; y++) {
|
|
72
|
+
const unsigned char red = pixels[(size_t)y * stride + (size_t)x * 4];
|
|
73
|
+
const unsigned char green = pixels[(size_t)y * stride + (size_t)x * 4 + 1];
|
|
74
|
+
const unsigned char blue = pixels[(size_t)y * stride + (size_t)x * 4 + 2];
|
|
75
|
+
if (blue > red + 20 && blue > green + 20) {
|
|
76
|
+
count++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
CGContextRelease(context);
|
|
81
|
+
return count;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} // namespace
|
|
85
|
+
|
|
86
|
+
TEST(appkit_selection_the_index_at_a_point_follows_the_text) {
|
|
87
|
+
@autoreleasepool {
|
|
88
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
89
|
+
|
|
90
|
+
EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(0, 10)], 0L);
|
|
91
|
+
EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(399, 10)],
|
|
92
|
+
(long)std::string("Selectable text").size());
|
|
93
|
+
|
|
94
|
+
const NSInteger middle = [view rnTextIndexAtPoint:CGPointMake(90, 10)];
|
|
95
|
+
EXPECT(middle > 0);
|
|
96
|
+
EXPECT(middle < (NSInteger)std::string("Selectable text").size());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
TEST(appkit_selection_a_view_with_no_paragraph_has_no_index) {
|
|
101
|
+
@autoreleasepool {
|
|
102
|
+
RnAppKitView *view = [RnAppKitView viewWithTag:3];
|
|
103
|
+
EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(10, 10)], -1L);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
TEST(appkit_selection_the_selected_text_is_what_the_clipboard_would_take) {
|
|
108
|
+
@autoreleasepool {
|
|
109
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
110
|
+
EXPECT([view rnSelectedText] == nil);
|
|
111
|
+
|
|
112
|
+
[view setRnTextSelectionStart:0 length:10];
|
|
113
|
+
EXPECT_EQ(std::string([view rnSelectedText].UTF8String), std::string("Selectable"));
|
|
114
|
+
|
|
115
|
+
// A range that runs past the end of a paragraph rebuilt under it: clamped
|
|
116
|
+
// rather than read past, which is a crash on a shorter string.
|
|
117
|
+
[view setRnTextSelectionStart:11 length:1000];
|
|
118
|
+
EXPECT_EQ(std::string([view rnSelectedText].UTF8String), std::string("text"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
TEST(appkit_selection_the_highlight_is_drawn_under_the_selected_range) {
|
|
123
|
+
@autoreleasepool {
|
|
124
|
+
const CGSize size = CGSizeMake(400, 60);
|
|
125
|
+
RnTextLayout *layout = paragraphLayout("Selectable text", 400);
|
|
126
|
+
|
|
127
|
+
// Nothing selected: no wash anywhere, which is the control for the two
|
|
128
|
+
// assertions below.
|
|
129
|
+
EXPECT_EQ(highlightPixels(layout, size, 0, 400), 0);
|
|
130
|
+
|
|
131
|
+
// The first four characters, which at 24pt is the left end of the line and
|
|
132
|
+
// nowhere near the right.
|
|
133
|
+
layout.selection = NSMakeRange(0, 4);
|
|
134
|
+
EXPECT(highlightPixels(layout, size, 0, 60) > 100);
|
|
135
|
+
EXPECT_EQ(highlightPixels(layout, size, 250, 400), 0);
|
|
136
|
+
|
|
137
|
+
// Clearing it takes the wash away rather than leaving the last one drawn.
|
|
138
|
+
layout.selection = NSMakeRange(0, 0);
|
|
139
|
+
EXPECT_EQ(highlightPixels(layout, size, 0, 400), 0);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
TEST(appkit_selection_a_paragraph_that_stops_being_selectable_loses_its_highlight) {
|
|
144
|
+
@autoreleasepool {
|
|
145
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
146
|
+
[view setRnTextSelectionStart:0 length:6];
|
|
147
|
+
[view setRnTextSelectable:NO];
|
|
148
|
+
EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
TEST(appkit_selection_a_rebuilt_paragraph_keeps_a_selection_of_the_same_text) {
|
|
153
|
+
@autoreleasepool {
|
|
154
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
155
|
+
[view setRnTextSelectionStart:0 length:10];
|
|
156
|
+
|
|
157
|
+
// Every mutation that touches a paragraph builds a new layout, a parent
|
|
158
|
+
// re-rendering being enough, so dropping the selection each time would make
|
|
159
|
+
// it impossible to keep one in a live app.
|
|
160
|
+
[view setRnTextLayout:paragraphLayout("Selectable text", 400)];
|
|
161
|
+
EXPECT_EQ((long)[view rnTextSelectionLength], 10L);
|
|
162
|
+
|
|
163
|
+
// Different text is a different string, and the old offsets would highlight
|
|
164
|
+
// whatever now sits at them.
|
|
165
|
+
[view setRnTextLayout:paragraphLayout("Something else entirely", 400)];
|
|
166
|
+
EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
TEST(appkit_selection_a_drag_across_selectable_text_selects_it) {
|
|
171
|
+
@autoreleasepool {
|
|
172
|
+
basalt::AppKitMountingManager manager;
|
|
173
|
+
RnAppKitView *root = manager.createSurfaceRoot(1);
|
|
174
|
+
root.frame = NSMakeRect(0, 0, 400, 60);
|
|
175
|
+
|
|
176
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
177
|
+
[root addSubview:view];
|
|
178
|
+
|
|
179
|
+
basalt::AppKitTouchDispatcher dispatcher(&manager, root);
|
|
180
|
+
|
|
181
|
+
// Ten steps from the left edge to the middle of the line, which is a sweep
|
|
182
|
+
// rather than a tap and so claims the gesture.
|
|
183
|
+
dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
|
|
184
|
+
EXPECT([view rnTextSelectionLength] > 0);
|
|
185
|
+
EXPECT_EQ((long)[view rnTextSelectionStart], 0L);
|
|
186
|
+
|
|
187
|
+
// What `Copy` and Cmd+C would take, through the provider the dispatcher
|
|
188
|
+
// installs: the same text the view reports.
|
|
189
|
+
EXPECT_EQ(basalt::selectedText(), std::string([view rnSelectedText].UTF8String));
|
|
190
|
+
|
|
191
|
+
// And a tap afterwards clears it, which is how a person dismisses a
|
|
192
|
+
// highlight.
|
|
193
|
+
dispatcher.synthesiseTap(200, 10);
|
|
194
|
+
EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
|
|
195
|
+
EXPECT(basalt::selectedText().empty());
|
|
196
|
+
|
|
197
|
+
manager.destroySurfaceRoot(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
TEST(appkit_selection_a_drag_across_text_that_is_not_selectable_selects_nothing) {
|
|
202
|
+
@autoreleasepool {
|
|
203
|
+
basalt::AppKitMountingManager manager;
|
|
204
|
+
RnAppKitView *root = manager.createSurfaceRoot(1);
|
|
205
|
+
root.frame = NSMakeRect(0, 0, 400, 60);
|
|
206
|
+
|
|
207
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, NO);
|
|
208
|
+
[root addSubview:view];
|
|
209
|
+
|
|
210
|
+
basalt::AppKitTouchDispatcher dispatcher(&manager, root);
|
|
211
|
+
dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
|
|
212
|
+
EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
|
|
213
|
+
|
|
214
|
+
manager.destroySurfaceRoot(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
TEST(appkit_selection_a_backwards_drag_selects_from_where_it_ended) {
|
|
219
|
+
@autoreleasepool {
|
|
220
|
+
basalt::AppKitMountingManager manager;
|
|
221
|
+
RnAppKitView *root = manager.createSurfaceRoot(1);
|
|
222
|
+
root.frame = NSMakeRect(0, 0, 400, 60);
|
|
223
|
+
|
|
224
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
225
|
+
[root addSubview:view];
|
|
226
|
+
|
|
227
|
+
basalt::AppKitTouchDispatcher dispatcher(&manager, root);
|
|
228
|
+
dispatcher.synthesiseDrag(150, 10, 2, 10, 10);
|
|
229
|
+
EXPECT_EQ((long)[view rnTextSelectionStart], 0L);
|
|
230
|
+
EXPECT([view rnTextSelectionLength] > 0);
|
|
231
|
+
|
|
232
|
+
manager.destroySurfaceRoot(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
TEST(appkit_selection_the_copy_role_copies_the_selection) {
|
|
237
|
+
@autoreleasepool {
|
|
238
|
+
// What Cmd+C performs on this host: the application menu's Copy item
|
|
239
|
+
// carries the key equivalent and performs the role, so there is no second
|
|
240
|
+
// key path. The role used to mean "whatever the responder chain will take
|
|
241
|
+
// a `copy:` for", which is a field and never a paragraph.
|
|
242
|
+
basalt::AppKitMountingManager manager;
|
|
243
|
+
RnAppKitView *root = manager.createSurfaceRoot(1);
|
|
244
|
+
root.frame = NSMakeRect(0, 0, 400, 60);
|
|
245
|
+
|
|
246
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
247
|
+
[root addSubview:view];
|
|
248
|
+
|
|
249
|
+
basalt::AppKitTouchDispatcher dispatcher(&manager, root);
|
|
250
|
+
const std::string original = basalt::clipboardText();
|
|
251
|
+
|
|
252
|
+
dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
|
|
253
|
+
const std::string expected = std::string([view rnSelectedText].UTF8String);
|
|
254
|
+
EXPECT(!expected.empty());
|
|
255
|
+
|
|
256
|
+
basalt::performMenuRole("copy");
|
|
257
|
+
EXPECT_EQ(basalt::clipboardText(), expected);
|
|
258
|
+
|
|
259
|
+
basalt::setClipboardText(original);
|
|
260
|
+
manager.destroySurfaceRoot(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- What a screen reader can read -------------------------------------------
|
|
265
|
+
//
|
|
266
|
+
// The other half of this entry. A paragraph is an RnTextLayout drawn into a
|
|
267
|
+
// view, so VoiceOver had a label for it and nothing to move through; these are
|
|
268
|
+
// the NSAccessibility answers added for that, and the selection one is the
|
|
269
|
+
// pair of the GTK suite's. Indices are UTF-16 code units everywhere here,
|
|
270
|
+
// which is what NSAttributedString counts and what VoiceOver asks in.
|
|
271
|
+
|
|
272
|
+
namespace {
|
|
273
|
+
|
|
274
|
+
// An emoji outside the basic plane, which is two UTF-16 code units: text where
|
|
275
|
+
// a count of characters and a count of code units disagree, so an assertion
|
|
276
|
+
// cannot pass by accident.
|
|
277
|
+
const char *const kWithEmoji = "a \xf0\x9f\x8e\xb8 b";
|
|
278
|
+
|
|
279
|
+
} // namespace
|
|
280
|
+
|
|
281
|
+
TEST(appkit_selection_a_paragraph_hands_a_screen_reader_its_text) {
|
|
282
|
+
RnAppKitView *view = paragraphView("hello world", 400, 60, YES);
|
|
283
|
+
EXPECT_EQ((int)[view accessibilityNumberOfCharacters], 11);
|
|
284
|
+
EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(6, 5)] UTF8String]),
|
|
285
|
+
std::string("world"));
|
|
286
|
+
// Past the end is clamped rather than throwing, which is what VoiceOver asks
|
|
287
|
+
// for the moment a paragraph shrinks under it.
|
|
288
|
+
EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(6, 99)] UTF8String]),
|
|
289
|
+
std::string("world"));
|
|
290
|
+
EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(99, 1)] UTF8String]),
|
|
291
|
+
std::string(""));
|
|
292
|
+
// The attributed form carries the run's font, which is what the plain one
|
|
293
|
+
// cannot say.
|
|
294
|
+
NSAttributedString *rich = [view accessibilityAttributedStringForRange:NSMakeRange(0, 5)];
|
|
295
|
+
EXPECT_EQ((int)rich.length, 5);
|
|
296
|
+
EXPECT(rich.length > 0 && [rich attribute:NSFontAttributeName atIndex:0 effectiveRange:nullptr] != nil);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
TEST(appkit_selection_a_view_with_no_paragraph_has_nothing_to_read) {
|
|
300
|
+
RnAppKitView *view = [RnAppKitView viewWithTag:3];
|
|
301
|
+
EXPECT_EQ((int)[view accessibilityNumberOfCharacters], 0);
|
|
302
|
+
EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(0, 5)] UTF8String]),
|
|
303
|
+
std::string(""));
|
|
304
|
+
EXPECT_EQ((int)[view accessibilitySelectedTextRange].length, 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
TEST(appkit_selection_a_selection_is_what_a_screen_reader_is_told_is_selected) {
|
|
308
|
+
RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
|
|
309
|
+
[view setRnTextSelectionStart:0 length:10];
|
|
310
|
+
|
|
311
|
+
EXPECT_EQ(std::string([[view accessibilitySelectedText] UTF8String]), std::string("Selectable"));
|
|
312
|
+
const NSRange range = [view accessibilitySelectedTextRange];
|
|
313
|
+
EXPECT_EQ((int)range.location, 0);
|
|
314
|
+
EXPECT_EQ((int)range.length, 10);
|
|
315
|
+
// The same span said twice: what the clipboard would take is what VoiceOver
|
|
316
|
+
// is told is selected.
|
|
317
|
+
EXPECT_EQ(std::string([[view accessibilityStringForRange:range] UTF8String]),
|
|
318
|
+
std::string([[view accessibilitySelectedText] UTF8String]));
|
|
319
|
+
|
|
320
|
+
[view setRnTextSelectionStart:0 length:0];
|
|
321
|
+
EXPECT_EQ((int)[view accessibilitySelectedTextRange].length, 0);
|
|
322
|
+
EXPECT_EQ(std::string([[view accessibilitySelectedText] UTF8String]), std::string(""));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
TEST(appkit_selection_a_screen_reader_can_ask_which_line_an_index_is_on) {
|
|
326
|
+
// Narrow enough to wrap, so there is more than one line to be on.
|
|
327
|
+
RnAppKitView *view = paragraphView("hello wonderful world", 120, 200, YES);
|
|
328
|
+
const NSRange first = [view accessibilityRangeForLine:0];
|
|
329
|
+
EXPECT_EQ((int)first.location, 0);
|
|
330
|
+
EXPECT(first.length > 0);
|
|
331
|
+
EXPECT(first.length < 21);
|
|
332
|
+
EXPECT_EQ((int)[view accessibilityLineForIndex:0], 0);
|
|
333
|
+
// An index on the second line is on a later line than one on the first,
|
|
334
|
+
// which is the claim that does not depend on where this font wraps.
|
|
335
|
+
EXPECT([view accessibilityLineForIndex:(NSInteger)NSMaxRange(first)] > 0);
|
|
336
|
+
// A line nobody has is NSNotFound rather than an empty range at zero.
|
|
337
|
+
EXPECT_EQ((int)[view accessibilityRangeForLine:99].location, (int)NSNotFound);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
TEST(appkit_selection_an_index_is_a_whole_character_rather_than_half_a_pair) {
|
|
341
|
+
RnAppKitView *view = paragraphView(kWithEmoji, 400, 60, YES);
|
|
342
|
+
// "a ", then the guitar, which is two code units and one character.
|
|
343
|
+
const NSRange composed = [view accessibilityRangeForIndex:2];
|
|
344
|
+
EXPECT_EQ((int)composed.location, 2);
|
|
345
|
+
EXPECT_EQ((int)composed.length, 2);
|
|
346
|
+
// Asked about the second half, the answer is still the whole thing.
|
|
347
|
+
EXPECT_EQ((int)[view accessibilityRangeForIndex:3].location, 2);
|
|
348
|
+
EXPECT_EQ((int)[view accessibilityRangeForIndex:99].location, (int)NSNotFound);
|
|
349
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
#include "TestHarness.h"
|
|
7
7
|
|
|
8
|
+
#include "MenuModel.h"
|
|
8
9
|
#include "PlatformServices.h"
|
|
9
10
|
|
|
10
11
|
#import <Cocoa/Cocoa.h>
|
|
@@ -52,6 +53,55 @@ TEST(can_open_url_answers_by_scheme) {
|
|
|
52
53
|
// What the alert needs proving about it is that it does not block: it is called
|
|
53
54
|
// from the JavaScript thread, and a modal run loop there would freeze every
|
|
54
55
|
// mount, timer and animation frame until somebody clicked. That is checked by
|
|
55
|
-
//
|
|
56
|
+
// e2e/alert.js instead, which keeps a 300ms heartbeat running and shows an alert
|
|
56
57
|
// half a second in -- the ticks carry straight on past it, which a blocking
|
|
57
58
|
// implementation could not do.
|
|
59
|
+
|
|
60
|
+
// A menu role reaching the field that has focus, in an app that is not the
|
|
61
|
+
// active one. See AppKitAppWindow.h.
|
|
62
|
+
//
|
|
63
|
+
// `performMenuRole` used to be one line, `[NSApp sendAction:selector to:nil
|
|
64
|
+
// from:nil]`, which walks the responder chain from the **key window's** first
|
|
65
|
+
// responder. A background app has no key window, so a `paste` role reached
|
|
66
|
+
// nothing at all -- and this suite is a background app, which is what makes the
|
|
67
|
+
// test possible without a harness around it.
|
|
68
|
+
//
|
|
69
|
+
// Uses the real clipboard and puts back what was there, the same courtesy
|
|
70
|
+
// `clipboard_round_trips` above extends.
|
|
71
|
+
TEST(a_menu_role_reaches_the_focused_field_without_the_app_being_active) {
|
|
72
|
+
@autoreleasepool {
|
|
73
|
+
const std::string original = basalt::clipboardText();
|
|
74
|
+
|
|
75
|
+
NSWindow *window = [[NSWindow alloc]
|
|
76
|
+
initWithContentRect:NSMakeRect(0, 0, 320, 80)
|
|
77
|
+
styleMask:NSWindowStyleMaskTitled
|
|
78
|
+
backing:NSBackingStoreBuffered
|
|
79
|
+
defer:NO];
|
|
80
|
+
NSTextField *field = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 300, 24)];
|
|
81
|
+
[window.contentView addSubview:field];
|
|
82
|
+
// Not released when closed, which is the same line main_appkit.mm needs for
|
|
83
|
+
// the windows the host makes and for the same reason: `close` on a window
|
|
84
|
+
// whose `releasedWhenClosed` is YES -- the default for one built in code --
|
|
85
|
+
// releases it while ARC still holds a reference, and the second release
|
|
86
|
+
// comes when the pool drains.
|
|
87
|
+
//
|
|
88
|
+
// That is undefined behaviour, so it did what undefined behaviour does: it
|
|
89
|
+
// passed here and segfaulted on CI's Mac, inside `objc_autoreleasePoolPop`
|
|
90
|
+
// with no test name, until the harness started flushing.
|
|
91
|
+
window.releasedWhenClosed = NO;
|
|
92
|
+
[window orderFront:nil];
|
|
93
|
+
// First responder without the app being active, which AppKit allows: what
|
|
94
|
+
// it does not do is make the window *key*.
|
|
95
|
+
EXPECT([window makeFirstResponder:field]);
|
|
96
|
+
EXPECT(NSApp.keyWindow == nil || NSApp.keyWindow == window);
|
|
97
|
+
|
|
98
|
+
basalt::setClipboardText("pasted by a role");
|
|
99
|
+
basalt::performMenuRole("paste");
|
|
100
|
+
|
|
101
|
+
EXPECT_EQ(std::string(field.stringValue.UTF8String), std::string("pasted by a role"));
|
|
102
|
+
|
|
103
|
+
[window orderOut:nil];
|
|
104
|
+
[window close];
|
|
105
|
+
basalt::setClipboardText(original);
|
|
106
|
+
}
|
|
107
|
+
}
|