@react-native-harness/ui 1.0.0-alpha.22

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.
@@ -0,0 +1,526 @@
1
+ #import "HarnessUI.h"
2
+ #import "ViewQueryHelper.h"
3
+ #import <QuartzCore/QuartzCore.h>
4
+ #import <React/RCTBridgeModule.h>
5
+ #import <React/RCTLog.h>
6
+ #import <UIKit/UIKit.h>
7
+ #import <objc/message.h>
8
+ #import <objc/runtime.h>
9
+
10
+ // Private UITouch methods for touch synthesis
11
+ @interface UITouch (Synthesis)
12
+ - (void)setPhase:(UITouchPhase)phase;
13
+ - (void)setTapCount:(NSUInteger)tapCount;
14
+ - (void)setIsTap:(BOOL)isTap;
15
+ - (void)setTimestamp:(NSTimeInterval)timestamp;
16
+ - (void)setWindow:(UIWindow *)window;
17
+ - (void)setView:(UIView *)view;
18
+ - (void)_setLocationInWindow:(CGPoint)location resetPrevious:(BOOL)reset;
19
+ - (void)_setIsFirstTouchForView:(BOOL)first;
20
+ - (void)_setTouchIdentifier:(unsigned int)identifier;
21
+ @end
22
+
23
+ // Private UIEvent methods for event synthesis
24
+ @interface UIEvent (Synthesis)
25
+ - (void)_addTouch:(UITouch *)touch forDelayedDelivery:(BOOL)delayed;
26
+ - (void)_clearTouches;
27
+ - (void)_setTimestamp:(NSTimeInterval)timestamp;
28
+ - (void)_setHIDEvent:(void *)event;
29
+ @end
30
+
31
+ // Private UIApplication method for event synthesis
32
+ @interface UIApplication (Synthesis)
33
+ - (UIEvent *)_touchesEvent;
34
+ @end
35
+
36
+ // Configuration constants
37
+ static const int kMaxRetryCount =
38
+ 10; // Maximum retries waiting for active touches
39
+ static const NSTimeInterval kRetryInterval = 0.05; // 50ms between retries
40
+ static const NSTimeInterval kSettleDelay =
41
+ 0.01; // 10ms delay for system to settle
42
+ static const NSTimeInterval kTapDuration =
43
+ 0.05; // 50ms between touch began and ended
44
+ static const NSTimeInterval kEventProcessingDelay =
45
+ 0.01; // 10ms delay after touch ended for React Native to process the event
46
+ static const NSTimeInterval kMinTapInterval =
47
+ 0.15; // 150ms minimum between taps
48
+
49
+ static unsigned int _touchIdCounter = 100;
50
+ static NSTimeInterval _lastTapTime = 0;
51
+
52
+ @implementation HarnessUI
53
+
54
+ #pragma mark - Window Access
55
+
56
+ - (UIWindow *)getActiveWindow {
57
+ return [ViewQueryHelper getActiveWindow];
58
+ }
59
+
60
+ #pragma mark - Touch Synthesis
61
+
62
+ - (void)sendTouchEvent:(UITouch *)touch
63
+ event:(UIEvent *)event
64
+ phase:(UITouchPhase)phase
65
+ point:(CGPoint)point {
66
+ NSTimeInterval timestamp = [[NSProcessInfo processInfo] systemUptime];
67
+
68
+ NSString *phaseName;
69
+ switch (phase) {
70
+ case UITouchPhaseBegan:
71
+ phaseName = @"began";
72
+ break;
73
+ case UITouchPhaseMoved:
74
+ phaseName = @"moved";
75
+ break;
76
+ case UITouchPhaseStationary:
77
+ phaseName = @"stationary";
78
+ break;
79
+ case UITouchPhaseEnded:
80
+ phaseName = @"ended";
81
+ break;
82
+ case UITouchPhaseCancelled:
83
+ phaseName = @"cancelled";
84
+ break;
85
+ default:
86
+ phaseName = @"unknown";
87
+ }
88
+ RCTLogInfo(@"[HarnessUI] Sending touch event: phase=%@ point=(%.2f, %.2f)",
89
+ phaseName, point.x, point.y);
90
+
91
+ [touch setPhase:phase];
92
+ [touch setTimestamp:timestamp];
93
+ [touch _setLocationInWindow:point resetPrevious:(phase == UITouchPhaseBegan)];
94
+
95
+ [event _clearTouches];
96
+ [event _addTouch:touch forDelayedDelivery:NO];
97
+ [event _setTimestamp:timestamp];
98
+
99
+ [[UIApplication sharedApplication] sendEvent:event];
100
+ }
101
+
102
+ - (BOOL)hasActiveTouches:(UIWindow *)window {
103
+ // Check if there are any active touches on the window
104
+ UIEvent *event = [[UIApplication sharedApplication] _touchesEvent];
105
+ NSSet *allTouches = [event allTouches];
106
+
107
+ for (UITouch *touch in allTouches) {
108
+ UITouchPhase phase = touch.phase;
109
+ if (phase == UITouchPhaseBegan || phase == UITouchPhaseMoved ||
110
+ phase == UITouchPhaseStationary) {
111
+ RCTLogInfo(@"[HarnessUI] Found active touch in phase %ld", (long)phase);
112
+ return YES;
113
+ }
114
+ }
115
+ return NO;
116
+ }
117
+
118
+ - (void)executeTapAtPoint:(CGPoint)point
119
+ retryCount:(int)retryCount
120
+ completion:(void (^)(void))completion {
121
+ UIWindow *window = [self getActiveWindow];
122
+ if (!window) {
123
+ RCTLogInfo(@"[HarnessUI] No active window found");
124
+ if (completion)
125
+ completion();
126
+ return;
127
+ }
128
+
129
+ // Check for active real touches - wait for them to finish
130
+ if ([self hasActiveTouches:window]) {
131
+ if (retryCount < kMaxRetryCount) {
132
+ RCTLogInfo(@"[HarnessUI] Active touches detected, waiting... (retry %d)",
133
+ retryCount);
134
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
135
+ (int64_t)(kRetryInterval * NSEC_PER_SEC)),
136
+ dispatch_get_main_queue(), ^{
137
+ [self executeTapAtPoint:point
138
+ retryCount:retryCount + 1
139
+ completion:completion];
140
+ });
141
+ return;
142
+ } else {
143
+ RCTLogInfo(
144
+ @"[HarnessUI] Timeout waiting for touches to end, proceeding anyway");
145
+ }
146
+ }
147
+
148
+ UIView *targetView = [window hitTest:point withEvent:nil];
149
+ if (!targetView) {
150
+ RCTLogInfo(@"[HarnessUI] No view at point (%.2f, %.2f)", point.x, point.y);
151
+ if (completion)
152
+ completion();
153
+ return;
154
+ }
155
+
156
+ RCTLogInfo(@"[HarnessUI] Target view: %@", targetView);
157
+
158
+ // Get the internal touches event from UIApplication
159
+ UIEvent *event = [[UIApplication sharedApplication] _touchesEvent];
160
+
161
+ // Aggressively clear all existing state
162
+ [event _clearTouches];
163
+ if ([event respondsToSelector:@selector(_setHIDEvent:)]) {
164
+ [event _setHIDEvent:NULL];
165
+ }
166
+
167
+ // Small delay after clearing to let the system settle
168
+ dispatch_after(
169
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kSettleDelay * NSEC_PER_SEC)),
170
+ dispatch_get_main_queue(), ^{
171
+ // Create fresh touch with unique identifier (handle overflow)
172
+ UITouch *touch = [[UITouch alloc] init];
173
+ _touchIdCounter =
174
+ (_touchIdCounter >= UINT_MAX - 1) ? 100 : _touchIdCounter + 1;
175
+ unsigned int touchId = _touchIdCounter;
176
+
177
+ if ([touch respondsToSelector:@selector(_setTouchIdentifier:)]) {
178
+ [touch _setTouchIdentifier:touchId];
179
+ }
180
+
181
+ // Configure touch
182
+ [touch setWindow:window];
183
+ [touch setView:targetView];
184
+ [touch setTapCount:1];
185
+ if ([touch respondsToSelector:@selector(_setIsFirstTouchForView:)]) {
186
+ [touch _setIsFirstTouchForView:YES];
187
+ }
188
+ if ([touch respondsToSelector:@selector(setIsTap:)]) {
189
+ [touch setIsTap:YES];
190
+ }
191
+
192
+ // Re-fetch event (in case it changed) and clear again
193
+ UIEvent *freshEvent = [[UIApplication sharedApplication] _touchesEvent];
194
+ [freshEvent _clearTouches];
195
+
196
+ // Send began
197
+ [self sendTouchEvent:touch
198
+ event:freshEvent
199
+ phase:UITouchPhaseBegan
200
+ point:point];
201
+ RCTLogInfo(@"[HarnessUI] Sent touch began (id=%u)", touchId);
202
+
203
+ // Schedule ended phase - re-fetch event to avoid race condition with
204
+ // real touches
205
+ dispatch_after(
206
+ dispatch_time(DISPATCH_TIME_NOW,
207
+ (int64_t)(kTapDuration * NSEC_PER_SEC)),
208
+ dispatch_get_main_queue(), ^{
209
+ UIEvent *endEvent =
210
+ [[UIApplication sharedApplication] _touchesEvent];
211
+ [self sendTouchEvent:touch
212
+ event:endEvent
213
+ phase:UITouchPhaseEnded
214
+ point:point];
215
+ [endEvent _clearTouches];
216
+ RCTLogInfo(@"[HarnessUI] Tap completed (id=%u)", touchId);
217
+ // Wait for React Native to process the touch event
218
+ // and trigger JS callbacks before resolving
219
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
220
+ (int64_t)(kEventProcessingDelay *
221
+ NSEC_PER_SEC)),
222
+ dispatch_get_main_queue(), ^{
223
+ if (completion)
224
+ completion();
225
+ });
226
+ });
227
+ });
228
+ }
229
+
230
+ - (void)executeTapAtPoint:(CGPoint)point completion:(void (^)(void))completion {
231
+ [self executeTapAtPoint:point retryCount:0 completion:completion];
232
+ }
233
+
234
+ - (void)simulatePress:(double)x
235
+ y:(double)y
236
+ resolve:(RCTPromiseResolveBlock)resolve
237
+ reject:(RCTPromiseRejectBlock)reject {
238
+ RCTLogInfo(@"[HarnessUI] simulatePress called with x:%.2f y:%.2f", x, y);
239
+
240
+ dispatch_async(dispatch_get_main_queue(), ^{
241
+ NSTimeInterval now = CACurrentMediaTime();
242
+ NSTimeInterval timeSinceLastTap = now - _lastTapTime;
243
+ CGPoint point = CGPointMake(x, y);
244
+
245
+ // Completion block that actually resolves the promise
246
+ void (^completionBlock)(void) = ^{
247
+ // Check if tap was successful by verifying we found a window/view
248
+ UIWindow *window = [self getActiveWindow];
249
+ if (!window) {
250
+ reject(@"NO_WINDOW", @"No active window found", nil);
251
+ return;
252
+ }
253
+
254
+ UIView *targetView = [window hitTest:point withEvent:nil];
255
+ if (!targetView) {
256
+ reject(
257
+ @"NO_VIEW",
258
+ [NSString
259
+ stringWithFormat:@"No view found at point (%.2f, %.2f)", x, y],
260
+ nil);
261
+ return;
262
+ }
263
+
264
+ // Success - resolve with success info
265
+ resolve(@{@"success" : @YES, @"x" : @(x), @"y" : @(y)});
266
+ };
267
+
268
+ if (timeSinceLastTap < kMinTapInterval) {
269
+ NSTimeInterval delay = kMinTapInterval - timeSinceLastTap;
270
+ RCTLogInfo(@"[HarnessUI] Delaying tap by %.0fms", delay * 1000);
271
+ _lastTapTime = now + delay;
272
+
273
+ dispatch_after(
274
+ dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)),
275
+ dispatch_get_main_queue(), ^{
276
+ [self executeTapAtPoint:point completion:completionBlock];
277
+ });
278
+ } else {
279
+ _lastTapTime = now;
280
+ [self executeTapAtPoint:point completion:completionBlock];
281
+ }
282
+ });
283
+ }
284
+
285
+ #pragma mark - Query API
286
+
287
+ - (NSDictionary *)queryByTestId:(NSString *)testId {
288
+ RCTLogInfo(@"[HarnessUI] queryByTestId called with: %@", testId);
289
+
290
+ __block NSDictionary *result = nil;
291
+
292
+ if ([NSThread isMainThread]) {
293
+ ViewQueryResult *queryResult =
294
+ [ViewQueryHelper queryWithType:ViewQueryTypeTestId value:testId];
295
+ result = queryResult ? [queryResult toDictionary] : nil;
296
+ } else {
297
+ dispatch_sync(dispatch_get_main_queue(), ^{
298
+ ViewQueryResult *queryResult =
299
+ [ViewQueryHelper queryWithType:ViewQueryTypeTestId value:testId];
300
+ result = queryResult ? [queryResult toDictionary] : nil;
301
+ });
302
+ }
303
+
304
+ RCTLogInfo(@"[HarnessUI] queryByTestId result: %@", result);
305
+ return result;
306
+ }
307
+
308
+ - (NSDictionary *)queryByAccessibilityLabel:(NSString *)label {
309
+ RCTLogInfo(@"[HarnessUI] queryByAccessibilityLabel called with: %@", label);
310
+
311
+ __block NSDictionary *result = nil;
312
+
313
+ if ([NSThread isMainThread]) {
314
+ ViewQueryResult *queryResult =
315
+ [ViewQueryHelper queryWithType:ViewQueryTypeAccessibilityLabel
316
+ value:label];
317
+ result = queryResult ? [queryResult toDictionary] : nil;
318
+ } else {
319
+ dispatch_sync(dispatch_get_main_queue(), ^{
320
+ ViewQueryResult *queryResult =
321
+ [ViewQueryHelper queryWithType:ViewQueryTypeAccessibilityLabel
322
+ value:label];
323
+ result = queryResult ? [queryResult toDictionary] : nil;
324
+ });
325
+ }
326
+
327
+ RCTLogInfo(@"[HarnessUI] queryByAccessibilityLabel result: %@", result);
328
+ return result;
329
+ }
330
+
331
+ - (NSArray<NSDictionary *> *)queryAllByTestId:(NSString *)testId {
332
+ RCTLogInfo(@"[HarnessUI] queryAllByTestId called with: %@", testId);
333
+
334
+ __block NSArray<NSDictionary *> *result = @[];
335
+
336
+ if ([NSThread isMainThread]) {
337
+ NSArray<ViewQueryResult *> *queryResults =
338
+ [ViewQueryHelper queryAllWithType:ViewQueryTypeTestId value:testId];
339
+ NSMutableArray<NSDictionary *> *dicts =
340
+ [NSMutableArray arrayWithCapacity:queryResults.count];
341
+ for (ViewQueryResult *queryResult in queryResults) {
342
+ [dicts addObject:[queryResult toDictionary]];
343
+ }
344
+ result = dicts;
345
+ } else {
346
+ dispatch_sync(dispatch_get_main_queue(), ^{
347
+ NSArray<ViewQueryResult *> *queryResults =
348
+ [ViewQueryHelper queryAllWithType:ViewQueryTypeTestId value:testId];
349
+ NSMutableArray<NSDictionary *> *dicts =
350
+ [NSMutableArray arrayWithCapacity:queryResults.count];
351
+ for (ViewQueryResult *queryResult in queryResults) {
352
+ [dicts addObject:[queryResult toDictionary]];
353
+ }
354
+ result = dicts;
355
+ });
356
+ }
357
+
358
+ RCTLogInfo(@"[HarnessUI] queryAllByTestId result count: %lu",
359
+ (unsigned long)result.count);
360
+ return result;
361
+ }
362
+
363
+ - (NSArray<NSDictionary *> *)queryAllByAccessibilityLabel:(NSString *)label {
364
+ RCTLogInfo(@"[HarnessUI] queryAllByAccessibilityLabel called with: %@", label);
365
+
366
+ __block NSArray<NSDictionary *> *result = @[];
367
+
368
+ if ([NSThread isMainThread]) {
369
+ NSArray<ViewQueryResult *> *queryResults =
370
+ [ViewQueryHelper queryAllWithType:ViewQueryTypeAccessibilityLabel
371
+ value:label];
372
+ NSMutableArray<NSDictionary *> *dicts =
373
+ [NSMutableArray arrayWithCapacity:queryResults.count];
374
+ for (ViewQueryResult *queryResult in queryResults) {
375
+ [dicts addObject:[queryResult toDictionary]];
376
+ }
377
+ result = dicts;
378
+ } else {
379
+ dispatch_sync(dispatch_get_main_queue(), ^{
380
+ NSArray<ViewQueryResult *> *queryResults =
381
+ [ViewQueryHelper queryAllWithType:ViewQueryTypeAccessibilityLabel
382
+ value:label];
383
+ NSMutableArray<NSDictionary *> *dicts =
384
+ [NSMutableArray arrayWithCapacity:queryResults.count];
385
+ for (ViewQueryResult *queryResult in queryResults) {
386
+ [dicts addObject:[queryResult toDictionary]];
387
+ }
388
+ result = dicts;
389
+ });
390
+ }
391
+
392
+ RCTLogInfo(@"[HarnessUI] queryAllByAccessibilityLabel result count: %lu",
393
+ (unsigned long)result.count);
394
+ return result;
395
+ }
396
+
397
+ #pragma mark - Screenshot API
398
+
399
+ - (void)captureScreenshot:(JS::NativeHarnessUI::ViewInfo *)bounds
400
+ resolve:(RCTPromiseResolveBlock)resolve
401
+ reject:(RCTPromiseRejectBlock)reject {
402
+ RCTLogInfo(@"[HarnessUI] captureScreenshot called");
403
+
404
+ CGRect captureRect = CGRectNull;
405
+
406
+ if (bounds) {
407
+ double width = bounds->width();
408
+ double height = bounds->height();
409
+
410
+ if (width > 0 && height > 0) {
411
+ captureRect = CGRectMake(bounds->x(), bounds->y(), width, height);
412
+ RCTLogInfo(@"[HarnessUI] Capturing region: x=%.2f y=%.2f w=%.2f h=%.2f",
413
+ captureRect.origin.x, captureRect.origin.y,
414
+ captureRect.size.width, captureRect.size.height);
415
+ }
416
+ } else {
417
+ RCTLogInfo(@"[HarnessUI] Capturing full window");
418
+ }
419
+
420
+ dispatch_async(dispatch_get_main_queue(), ^{
421
+ NSData *pngData = [ViewQueryHelper captureScreenshotWithBounds:captureRect];
422
+
423
+ if (pngData) {
424
+ // Return Base64 string for efficiency
425
+ NSString *base64String = [pngData base64EncodedStringWithOptions:0];
426
+ RCTLogInfo(@"[HarnessUI] Screenshot captured successfully (%lu bytes)",
427
+ (unsigned long)pngData.length);
428
+ resolve(base64String);
429
+ } else {
430
+ RCTLogInfo(@"[HarnessUI] Screenshot capture returned nil");
431
+ resolve([NSNull null]);
432
+ }
433
+ });
434
+ }
435
+
436
+ #pragma mark - Text Input
437
+
438
+ - (UIResponder *)findFirstResponderInView:(UIView *)view {
439
+ if ([view isFirstResponder]) {
440
+ return view;
441
+ }
442
+ for (UIView *subview in view.subviews) {
443
+ UIResponder *responder = [self findFirstResponderInView:subview];
444
+ if (responder) {
445
+ return responder;
446
+ }
447
+ }
448
+ return nil;
449
+ }
450
+
451
+ - (void)typeChar:(NSString *)character
452
+ resolve:(RCTPromiseResolveBlock)resolve
453
+ reject:(RCTPromiseRejectBlock)reject {
454
+ RCTLogInfo(@"[HarnessUI] typeChar called with: %@", character);
455
+
456
+ dispatch_async(dispatch_get_main_queue(), ^{
457
+ UIWindow *window = [self getActiveWindow];
458
+ UIResponder *firstResponder = [self findFirstResponderInView:window];
459
+
460
+ if ([firstResponder conformsToProtocol:@protocol(UITextInput)]) {
461
+ id<UITextInput> textInput = (id<UITextInput>)firstResponder;
462
+ [textInput insertText:character];
463
+ RCTLogInfo(@"[HarnessUI] Inserted character: %@", character);
464
+ } else {
465
+ RCTLogInfo(@"[HarnessUI] No text input is focused");
466
+ }
467
+
468
+ // Small delay for React Native to process the change event
469
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
470
+ (int64_t)(kEventProcessingDelay * NSEC_PER_SEC)),
471
+ dispatch_get_main_queue(), ^{
472
+ resolve(nil);
473
+ });
474
+ });
475
+ }
476
+
477
+ - (void)blur:(JS::NativeHarnessUI::SpecBlurOptions &)options
478
+ resolve:(RCTPromiseResolveBlock)resolve
479
+ reject:(RCTPromiseRejectBlock)reject {
480
+ RCTLogInfo(@"[HarnessUI] blur called");
481
+
482
+ // Extract value before async dispatch to avoid dangling reference
483
+ BOOL shouldSubmitEditing = options.submitEditing().value_or(false);
484
+
485
+ dispatch_async(dispatch_get_main_queue(), ^{
486
+ UIWindow *window = [self getActiveWindow];
487
+ UIResponder *firstResponder = [self findFirstResponderInView:window];
488
+
489
+ if (firstResponder) {
490
+ if (shouldSubmitEditing) {
491
+ RCTLogInfo(@"[HarnessUI] Triggering submitEditing");
492
+ // Trigger submitEditing event if the responder supports it
493
+ // For UITextField, this simulates pressing Return
494
+ if ([firstResponder isKindOfClass:[UITextField class]]) {
495
+ UITextField *textField = (UITextField *)firstResponder;
496
+ [textField.delegate textFieldShouldReturn:textField];
497
+ }
498
+ }
499
+
500
+ // Resign first responder (triggers endEditing and blur events)
501
+ RCTLogInfo(@"[HarnessUI] Resigning first responder");
502
+ [firstResponder resignFirstResponder];
503
+ } else {
504
+ RCTLogInfo(@"[HarnessUI] No first responder found");
505
+ }
506
+
507
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
508
+ (int64_t)(kEventProcessingDelay * NSEC_PER_SEC)),
509
+ dispatch_get_main_queue(), ^{
510
+ resolve(nil);
511
+ });
512
+ });
513
+ }
514
+
515
+ #pragma mark - TurboModule plumbing
516
+
517
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
518
+ (const facebook::react::ObjCTurboModule::InitParams &)params {
519
+ return std::make_shared<facebook::react::NativeHarnessUISpecJSI>(params);
520
+ }
521
+
522
+ + (NSString *)moduleName {
523
+ return @"HarnessUI";
524
+ }
525
+
526
+ @end
@@ -0,0 +1,64 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /**
6
+ * Enum defining the types of queries that can be performed on the view hierarchy.
7
+ * Add new query types here to extend functionality.
8
+ */
9
+ typedef NS_ENUM(NSInteger, ViewQueryType) {
10
+ ViewQueryTypeTestId, // Matches accessibilityIdentifier
11
+ ViewQueryTypeAccessibilityLabel // Matches accessibilityLabel
12
+ };
13
+
14
+ /**
15
+ * Represents information about a found view.
16
+ */
17
+ @interface ViewQueryResult : NSObject
18
+
19
+ @property (nonatomic, assign) CGFloat x;
20
+ @property (nonatomic, assign) CGFloat y;
21
+ @property (nonatomic, assign) CGFloat width;
22
+ @property (nonatomic, assign) CGFloat height;
23
+
24
+ - (NSDictionary *)toDictionary;
25
+
26
+ @end
27
+
28
+ /**
29
+ * Helper class for querying the view hierarchy.
30
+ * Provides reusable query logic for finding views by various criteria.
31
+ */
32
+ @interface ViewQueryHelper : NSObject
33
+
34
+ /**
35
+ * Returns the currently active window.
36
+ */
37
+ + (nullable UIWindow *)getActiveWindow;
38
+
39
+ /**
40
+ * Finds the first view matching the query criteria.
41
+ * @param queryType The type of query to perform.
42
+ * @param value The value to match against.
43
+ * @return ViewQueryResult if found, nil otherwise.
44
+ */
45
+ + (nullable ViewQueryResult *)queryWithType:(ViewQueryType)queryType value:(NSString *)value;
46
+
47
+ /**
48
+ * Finds all views matching the query criteria.
49
+ * @param queryType The type of query to perform.
50
+ * @param value The value to match against.
51
+ * @return Array of ViewQueryResult objects.
52
+ */
53
+ + (NSArray<ViewQueryResult *> *)queryAllWithType:(ViewQueryType)queryType value:(NSString *)value;
54
+
55
+ /**
56
+ * Captures a screenshot of the window or a specific region.
57
+ * @param bounds Optional CGRect for capturing a specific region. Pass CGRectNull for full window.
58
+ * @return NSData containing PNG image data, or nil on failure.
59
+ */
60
+ + (nullable NSData *)captureScreenshotWithBounds:(CGRect)bounds;
61
+
62
+ @end
63
+
64
+ NS_ASSUME_NONNULL_END