@react-native-harness/ui 1.0.0-alpha.23 → 1.0.0-alpha.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +25 -11
  2. package/android/src/main/java/com/harnessui/HarnessUIModule.kt +26 -16
  3. package/android/src/main/java/com/harnessui/UIHelperImpl.kt +136 -41
  4. package/android/src/main/java/com/harnessui/ViewQueryHelper.kt +36 -15
  5. package/android/src/main/java/com/harnessui/ViewRegistry.kt +51 -0
  6. package/dist/HarnessUI.d.ts +17 -0
  7. package/dist/HarnessUI.d.ts.map +1 -0
  8. package/dist/HarnessUI.js +2 -0
  9. package/dist/HarnessUI.web.d.ts +14 -0
  10. package/dist/HarnessUI.web.d.ts.map +1 -0
  11. package/dist/HarnessUI.web.js +46 -0
  12. package/dist/NativeHarness.d.ts +22 -0
  13. package/dist/NativeHarness.d.ts.map +1 -0
  14. package/dist/NativeHarness.js +2 -0
  15. package/dist/NativeHarnessUI.d.ts +10 -37
  16. package/dist/NativeHarnessUI.d.ts.map +1 -1
  17. package/dist/WebHarness.d.ts +14 -0
  18. package/dist/WebHarness.d.ts.map +1 -0
  19. package/dist/WebHarness.js +53 -0
  20. package/dist/WebHarnessUI.d.ts +15 -0
  21. package/dist/WebHarnessUI.d.ts.map +1 -0
  22. package/dist/WebHarnessUI.js +81 -0
  23. package/dist/harness/NativeHarness.d.ts +17 -0
  24. package/dist/harness/NativeHarness.d.ts.map +1 -0
  25. package/dist/harness/NativeHarness.js +2 -0
  26. package/dist/harness/WebHarness.d.ts +14 -0
  27. package/dist/harness/WebHarness.d.ts.map +1 -0
  28. package/dist/harness/WebHarness.js +53 -0
  29. package/dist/harness/index.d.ts +4 -0
  30. package/dist/harness/index.d.ts.map +1 -0
  31. package/dist/harness/index.js +8 -0
  32. package/dist/harness/native-harness.d.ts +17 -0
  33. package/dist/harness/native-harness.d.ts.map +1 -0
  34. package/dist/harness/native-harness.js +2 -0
  35. package/dist/harness/web-harness.d.ts +14 -0
  36. package/dist/harness/web-harness.d.ts.map +1 -0
  37. package/dist/harness/web-harness.js +53 -0
  38. package/dist/harness.d.ts +4 -0
  39. package/dist/harness.d.ts.map +1 -0
  40. package/dist/harness.js +8 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/screen.d.ts +9 -7
  44. package/dist/screen.d.ts.map +1 -1
  45. package/dist/screen.js +50 -20
  46. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +48 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +1 -0
  50. package/dist/userEvent.d.ts.map +1 -1
  51. package/dist/userEvent.js +10 -19
  52. package/ios/HarnessUI.mm +58 -8
  53. package/ios/ViewQueryHelper.h +9 -0
  54. package/ios/ViewQueryHelper.mm +24 -1
  55. package/ios/ViewRegistry.h +8 -0
  56. package/ios/ViewRegistry.mm +33 -0
  57. package/package.json +2 -2
  58. package/src/NativeHarnessUI.ts +11 -45
  59. package/src/WebHarnessUI.ts +113 -0
  60. package/src/harness.ts +12 -0
  61. package/src/index.ts +1 -1
  62. package/src/screen.ts +64 -27
  63. package/src/types.ts +54 -0
  64. package/src/userEvent.ts +10 -19
package/dist/userEvent.js CHANGED
@@ -1,4 +1,4 @@
1
- import NativeHarnessUI from './NativeHarnessUI.js';
1
+ import HarnessUI from './harness.js';
2
2
  /**
3
3
  * Flushes pending events on the JS event loop.
4
4
  * This ensures that any events triggered by native code (like onPress callbacks)
@@ -23,45 +23,36 @@ const flushEvents = () => {
23
23
  const createUserEvent = () => {
24
24
  return {
25
25
  press: async (element) => {
26
- // Calculate center point of the element
27
- const centerX = element.x + element.width / 2;
28
- const centerY = element.y + element.height / 2;
29
- await NativeHarnessUI.simulatePress(centerX, centerY);
26
+ // We pass 0, 0 as coordinates when nativeId is provided.
27
+ // Native side will resolve the view and calculate the center.
28
+ await HarnessUI.simulatePress(element.nativeId, 0, 0);
30
29
  // Flush pending events to ensure onPress and other callbacks are processed
31
30
  await flushEvents();
32
31
  },
33
32
  pressAt: async (x, y) => {
34
- await NativeHarnessUI.simulatePress(x, y);
33
+ await HarnessUI.simulatePress('', x, y);
35
34
  // Flush pending events to ensure onPress and other callbacks are processed
36
35
  await flushEvents();
37
36
  },
38
37
  type: async (element, text, options) => {
39
38
  // Press to focus the element (triggers pressIn/pressOut unless skipPress is true)
40
- // Note: Currently we always press to focus, the skipPress option would need
41
- // additional implementation in simulatePress to avoid firing press events
42
39
  if (!options?.skipPress) {
43
- // Calculate center point of the element
44
- const centerX = element.x + element.width / 2;
45
- const centerY = element.y + element.height / 2;
46
- await NativeHarnessUI.simulatePress(centerX, centerY);
40
+ await HarnessUI.simulatePress(element.nativeId, 0, 0);
47
41
  await flushEvents();
48
42
  }
49
43
  else {
50
- // Still need to press to focus, but ideally without press events
51
- // For now, we press anyway - future enhancement could add a focusOnly method
52
- const centerX = element.x + element.width / 2;
53
- const centerY = element.y + element.height / 2;
54
- await NativeHarnessUI.simulatePress(centerX, centerY);
44
+ // Still need to press to focus
45
+ await HarnessUI.simulatePress(element.nativeId, 0, 0);
55
46
  await flushEvents();
56
47
  }
57
48
  // Type each character one by one
58
49
  for (const char of text) {
59
- await NativeHarnessUI.typeChar(char);
50
+ await HarnessUI.typeChar(char);
60
51
  await flushEvents(); // Let onChangeText fire
61
52
  }
62
53
  // Blur (triggers endEditing and blur unless skipBlur)
63
54
  if (!options?.skipBlur) {
64
- await NativeHarnessUI.blur({
55
+ await HarnessUI.blur({
65
56
  submitEditing: options?.submitEditing ?? false,
66
57
  });
67
58
  await flushEvents();
package/ios/HarnessUI.mm CHANGED
@@ -1,5 +1,6 @@
1
1
  #import "HarnessUI.h"
2
2
  #import "ViewQueryHelper.h"
3
+ #import "ViewRegistry.h"
3
4
  #import <QuartzCore/QuartzCore.h>
4
5
  #import <React/RCTBridgeModule.h>
5
6
  #import <React/RCTLog.h>
@@ -231,17 +232,34 @@ static NSTimeInterval _lastTapTime = 0;
231
232
  [self executeTapAtPoint:point retryCount:0 completion:completion];
232
233
  }
233
234
 
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);
235
+ - (void)simulatePress:(NSString *)nativeId
236
+ x:(double)x
237
+ y:(double)y
238
+ resolve:(RCTPromiseResolveBlock)resolve
239
+ reject:(RCTPromiseRejectBlock)reject {
240
+ RCTLogInfo(@"[HarnessUI] simulatePress called with nativeId:%@ x:%.2f y:%.2f", nativeId, x, y);
239
241
 
240
242
  dispatch_async(dispatch_get_main_queue(), ^{
241
243
  NSTimeInterval now = CACurrentMediaTime();
242
244
  NSTimeInterval timeSinceLastTap = now - _lastTapTime;
243
245
  CGPoint point = CGPointMake(x, y);
244
246
 
247
+ if (nativeId && nativeId.length > 0) {
248
+ UIView *view = [ViewRegistry getView:nativeId];
249
+ if (view) {
250
+ UIWindow *window = [self getActiveWindow]; // Or view.window? view.window is safer if attached.
251
+ if (!window) window = view.window;
252
+
253
+ if (window) {
254
+ CGRect frameInWindow = [view convertRect:view.bounds toView:window];
255
+ point = CGPointMake(CGRectGetMidX(frameInWindow), CGRectGetMidY(frameInWindow));
256
+ RCTLogInfo(@"[HarnessUI] Resolved view %@ to point (%.2f, %.2f)", nativeId, point.x, point.y);
257
+ }
258
+ } else {
259
+ RCTLogWarn(@"[HarnessUI] View with nativeId %@ not found or collected. Using provided coordinates.", nativeId);
260
+ }
261
+ }
262
+
245
263
  // Completion block that actually resolves the promise
246
264
  void (^completionBlock)(void) = ^{
247
265
  // Check if tap was successful by verifying we found a window/view
@@ -402,23 +420,55 @@ static NSTimeInterval _lastTapTime = 0;
402
420
  RCTLogInfo(@"[HarnessUI] captureScreenshot called");
403
421
 
404
422
  CGRect captureRect = CGRectNull;
423
+ NSString *nativeId = nil;
405
424
 
406
425
  if (bounds) {
426
+ if (bounds->nativeId().length > 0) {
427
+ nativeId = bounds->nativeId();
428
+ }
429
+
407
430
  double width = bounds->width();
408
431
  double height = bounds->height();
409
432
 
410
433
  if (width > 0 && height > 0) {
411
434
  captureRect = CGRectMake(bounds->x(), bounds->y(), width, height);
412
- RCTLogInfo(@"[HarnessUI] Capturing region: x=%.2f y=%.2f w=%.2f h=%.2f",
435
+ RCTLogInfo(@"[HarnessUI] Capturing region: x=%.2f y=%.2f w=%.2f h=%.2f nativeId=%@",
413
436
  captureRect.origin.x, captureRect.origin.y,
414
- captureRect.size.width, captureRect.size.height);
437
+ captureRect.size.width, captureRect.size.height, nativeId);
415
438
  }
416
439
  } else {
417
440
  RCTLogInfo(@"[HarnessUI] Capturing full window");
418
441
  }
419
442
 
420
443
  dispatch_async(dispatch_get_main_queue(), ^{
421
- NSData *pngData = [ViewQueryHelper captureScreenshotWithBounds:captureRect];
444
+ NSData *pngData = nil;
445
+
446
+ if (nativeId) {
447
+ UIView *targetView = [ViewRegistry getView:nativeId];
448
+ if (targetView) {
449
+ UIWindow *window = [self getActiveWindow];
450
+ if (window) {
451
+ CGRect frameInWindow = [targetView convertRect:targetView.bounds toView:window];
452
+ BOOL isFullyOnScreen = CGRectContainsRect(window.bounds, frameInWindow);
453
+
454
+ if (isFullyOnScreen) {
455
+ RCTLogInfo(@"[HarnessUI] View %@ is fully on screen. Using screen-based screenshotting.", nativeId);
456
+ pngData = [ViewQueryHelper captureScreenshotWithBounds:frameInWindow];
457
+ } else {
458
+ RCTLogInfo(@"[HarnessUI] View %@ is partially/fully off screen. Using direct view render.", nativeId);
459
+ pngData = [ViewQueryHelper captureScreenshotOfView:targetView];
460
+ }
461
+ } else {
462
+ RCTLogWarn(@"[HarnessUI] No active window found for nativeId: %@. Using direct view render.", nativeId);
463
+ pngData = [ViewQueryHelper captureScreenshotOfView:targetView];
464
+ }
465
+ } else {
466
+ RCTLogWarn(@"[HarnessUI] View with nativeId %@ not found. Falling back to window bounds capture.", nativeId);
467
+ pngData = [ViewQueryHelper captureScreenshotWithBounds:captureRect];
468
+ }
469
+ } else {
470
+ pngData = [ViewQueryHelper captureScreenshotWithBounds:captureRect];
471
+ }
422
472
 
423
473
  if (pngData) {
424
474
  // Return Base64 string for efficiency
@@ -20,6 +20,7 @@ typedef NS_ENUM(NSInteger, ViewQueryType) {
20
20
  @property (nonatomic, assign) CGFloat y;
21
21
  @property (nonatomic, assign) CGFloat width;
22
22
  @property (nonatomic, assign) CGFloat height;
23
+ @property (nonatomic, copy) NSString *nativeId;
23
24
 
24
25
  - (NSDictionary *)toDictionary;
25
26
 
@@ -59,6 +60,14 @@ typedef NS_ENUM(NSInteger, ViewQueryType) {
59
60
  */
60
61
  + (nullable NSData *)captureScreenshotWithBounds:(CGRect)bounds;
61
62
 
63
+ /**
64
+ * Captures a screenshot of a specific view, rendering its layer directly.
65
+ * This allows capturing content that might be off-screen or larger than the window.
66
+ * @param view The view to capture.
67
+ * @return NSData containing PNG image data, or nil on failure.
68
+ */
69
+ + (nullable NSData *)captureScreenshotOfView:(UIView *)view;
70
+
62
71
  @end
63
72
 
64
73
  NS_ASSUME_NONNULL_END
@@ -1,4 +1,5 @@
1
1
  #import "ViewQueryHelper.h"
2
+ #import "ViewRegistry.h"
2
3
 
3
4
  // =============================================================================
4
5
  // ViewQueryResult Implementation
@@ -11,7 +12,8 @@
11
12
  @"x": @(self.x),
12
13
  @"y": @(self.y),
13
14
  @"width": @(self.width),
14
- @"height": @(self.height)
15
+ @"height": @(self.height),
16
+ @"nativeId": self.nativeId ?: @""
15
17
  };
16
18
  }
17
19
 
@@ -119,6 +121,7 @@
119
121
  result.y = frameInWindow.origin.y;
120
122
  result.width = frameInWindow.size.width;
121
123
  result.height = frameInWindow.size.height;
124
+ result.nativeId = [ViewRegistry registerView:view];
122
125
  return result;
123
126
  }
124
127
 
@@ -184,4 +187,24 @@
184
187
  return UIImagePNGRepresentation(image);
185
188
  }
186
189
 
190
+ + (nullable NSData *)captureScreenshotOfView:(UIView *)view {
191
+ if (!view) {
192
+ return nil;
193
+ }
194
+
195
+ // Use UIGraphicsImageRenderer to render the view's layer
196
+ UIGraphicsImageRendererFormat *format = [[UIGraphicsImageRendererFormat alloc] init];
197
+ format.scale = [UIScreen mainScreen].scale;
198
+ format.opaque = NO; // Allow transparency if the view has it
199
+
200
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:view.bounds.size format:format];
201
+
202
+ UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *context) {
203
+ // Render the view's layer directly into the context
204
+ [view.layer renderInContext:context.CGContext];
205
+ }];
206
+
207
+ return UIImagePNGRepresentation(image);
208
+ }
209
+
187
210
  @end
@@ -0,0 +1,8 @@
1
+ #import <UIKit/UIKit.h>
2
+
3
+ @interface ViewRegistry : NSObject
4
+
5
+ + (NSString *)registerView:(UIView *)view;
6
+ + (UIView *)getView:(NSString *)nativeId;
7
+
8
+ @end
@@ -0,0 +1,33 @@
1
+ #import "ViewRegistry.h"
2
+
3
+ @implementation ViewRegistry
4
+
5
+ + (NSMapTable<NSString *, UIView *> *)registry {
6
+ static NSMapTable *registry = nil;
7
+ static dispatch_once_t onceToken;
8
+ dispatch_once(&onceToken, ^{
9
+ // Key: Strong (NSString), Value: Weak (UIView)
10
+ // When UIView is deallocated, it is automatically removed from the map.
11
+ registry = [NSMapTable strongToWeakObjectsMapTable];
12
+ });
13
+ return registry;
14
+ }
15
+
16
+ + (NSString *)registerView:(UIView *)view {
17
+ if (!view) return nil;
18
+
19
+ // Generate a unique ID. Using a random UUID ensures freshness for each query.
20
+ NSString *nativeId = [[NSUUID UUID] UUIDString];
21
+
22
+ // Store in registry
23
+ [[self registry] setObject:view forKey:nativeId];
24
+
25
+ return nativeId;
26
+ }
27
+
28
+ + (UIView *)getView:(NSString *)nativeId {
29
+ if (!nativeId) return nil;
30
+ return [[self registry] objectForKey:nativeId];
31
+ }
32
+
33
+ @end
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@react-native-harness/ui",
3
3
  "description": "Native UI testing module for React Native Harness.",
4
- "version": "1.0.0-alpha.23",
4
+ "version": "1.0.0-alpha.25",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -38,7 +38,7 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "tslib": "^2.3.0",
41
- "@react-native-harness/runtime": "1.0.0-alpha.23"
41
+ "@react-native-harness/runtime": "1.0.0-alpha.25"
42
42
  },
43
43
  "devDependencies": {
44
44
  "react-native": "*"
@@ -1,66 +1,32 @@
1
1
  import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
2
 
3
3
  /**
4
- * Represents the position and dimensions of a view in screen coordinates (points/dp).
4
+ * Represents a bounding box in screen coordinates (points/dp).
5
5
  */
6
- export interface ViewInfo {
6
+ export interface BoundingBox {
7
7
  x: number;
8
8
  y: number;
9
9
  width: number;
10
10
  height: number;
11
11
  }
12
12
 
13
- interface Spec extends TurboModule {
14
- /**
15
- * Simulates a native press at the specified screen coordinates.
16
- * Returns a promise that resolves when the press action is complete.
17
- */
18
- simulatePress(x: number, y: number): Promise<void>;
13
+ /**
14
+ * Internal interface used for bridge communication.
15
+ * This needs to be exported for TurboModule codegen.
16
+ */
17
+ export interface ViewInfo extends BoundingBox {
18
+ nativeId: string;
19
+ }
19
20
 
20
- /**
21
- * Finds a view by its testID (accessibilityIdentifier on iOS, tag on Android).
22
- * Returns null if no view is found.
23
- */
21
+ interface Spec extends TurboModule {
22
+ simulatePress(nativeId: string, x: number, y: number): Promise<void>;
24
23
  queryByTestId(testId: string): ViewInfo | null;
25
-
26
- /**
27
- * Finds all views by testID (accessibilityIdentifier on iOS, tag on Android).
28
- * Returns an empty array if no views are found.
29
- */
30
24
  queryAllByTestId(testId: string): ViewInfo[];
31
-
32
- /**
33
- * Finds a view by its accessibility label.
34
- * Returns null if no view is found.
35
- */
36
25
  queryByAccessibilityLabel(label: string): ViewInfo | null;
37
-
38
- /**
39
- * Finds all views by accessibility label.
40
- * Returns an empty array if no views are found.
41
- */
42
26
  queryAllByAccessibilityLabel(label: string): ViewInfo[];
43
-
44
- /**
45
- * Captures a screenshot of the app UI.
46
- * @param bounds Optional bounds to capture a specific region. Pass null to capture the entire window.
47
- * @returns Promise resolving to Base64 encoded string containing PNG data, or null on failure.
48
- */
49
27
  captureScreenshot(bounds: ViewInfo | null): Promise<string | null>;
50
-
51
- /**
52
- * Types a single character into the currently focused text input.
53
- * If no text input is focused, this is a no-op.
54
- */
55
28
  typeChar(character: string): Promise<void>;
56
-
57
- /**
58
- * Blurs (resigns first responder from) the currently focused element.
59
- * Optionally triggers submitEditing event before blur.
60
- */
61
29
  blur(options: { submitEditing?: boolean }): Promise<void>;
62
30
  }
63
31
 
64
- export type HarnessUIModule = Spec;
65
-
66
32
  export default TurboModuleRegistry.getEnforcing<Spec>('HarnessUI');
@@ -0,0 +1,113 @@
1
+ import { type HarnessUIModule, type ViewInfo } from './types.js';
2
+
3
+ declare global {
4
+ interface Window {
5
+ __RN_HARNESS_CAPTURE_SCREENSHOT__: (
6
+ bounds: ViewInfo | null
7
+ ) => Promise<string | null>;
8
+ __RN_HARNESS_SIMULATE_PRESS__: (x: number, y: number) => Promise<void>;
9
+ __RN_HARNESS_TYPE_CHAR__: (character: string) => Promise<void>;
10
+ __RN_HARNESS_BLUR__: (options: {
11
+ submitEditing?: boolean;
12
+ }) => Promise<void>;
13
+ __RN_HARNESS_VIEW_REGISTRY__: Map<string, Element>;
14
+ }
15
+ }
16
+
17
+ if (!window.__RN_HARNESS_VIEW_REGISTRY__) {
18
+ window.__RN_HARNESS_VIEW_REGISTRY__ = new Map();
19
+ }
20
+
21
+ let nextId = 1;
22
+
23
+ const getElementViewInfo = (element: Element): ViewInfo => {
24
+ const rect = element.getBoundingClientRect();
25
+
26
+ let nativeId = (element as any).__harnessId;
27
+ if (!nativeId) {
28
+ nativeId = `view_${nextId++}`;
29
+ (element as any).__harnessId = nativeId;
30
+ window.__RN_HARNESS_VIEW_REGISTRY__.set(nativeId, element);
31
+ }
32
+
33
+ return {
34
+ x: rect.left,
35
+ y: rect.top,
36
+ width: rect.width,
37
+ height: rect.height,
38
+ nativeId,
39
+ };
40
+ };
41
+
42
+ const WebHarnessUI: HarnessUIModule = {
43
+ simulatePress: async (nativeId, x, y) => {
44
+ let targetX = x;
45
+ let targetY = y;
46
+
47
+ if (nativeId) {
48
+ const element = window.__RN_HARNESS_VIEW_REGISTRY__.get(nativeId);
49
+ if (element) {
50
+ const rect = element.getBoundingClientRect();
51
+ targetX = rect.left + rect.width / 2;
52
+ targetY = rect.top + rect.height / 2;
53
+ }
54
+ }
55
+
56
+ await window.__RN_HARNESS_SIMULATE_PRESS__(targetX, targetY);
57
+ },
58
+
59
+ queryByTestId: (testId) => {
60
+ const element = document.querySelector(`[data-testid="${testId}"]`);
61
+ return element ? getElementViewInfo(element) : null;
62
+ },
63
+
64
+ queryAllByTestId: (testId) => {
65
+ const elements = document.querySelectorAll(`[data-testid="${testId}"]`);
66
+ return Array.from(elements).map(getElementViewInfo);
67
+ },
68
+
69
+ queryByAccessibilityLabel: (label) => {
70
+ const element = document.querySelector(`[aria-label="${label}"]`);
71
+ return element ? getElementViewInfo(element) : null;
72
+ },
73
+
74
+ queryAllByAccessibilityLabel: (label) => {
75
+ const elements = document.querySelectorAll(`[aria-label="${label}"]`);
76
+ return Array.from(elements).map(getElementViewInfo);
77
+ },
78
+
79
+ captureScreenshot: async (bounds) => {
80
+ let captureBounds = bounds;
81
+ if (bounds?.nativeId && bounds.width === 0 && bounds.height === 0) {
82
+ const element = window.__RN_HARNESS_VIEW_REGISTRY__.get(bounds.nativeId);
83
+ if (element) {
84
+ captureBounds = getElementViewInfo(element);
85
+ }
86
+ }
87
+ return await window.__RN_HARNESS_CAPTURE_SCREENSHOT__(captureBounds);
88
+ },
89
+
90
+ typeChar: async (character) => {
91
+ await window.__RN_HARNESS_TYPE_CHAR__(character);
92
+ },
93
+
94
+ blur: async (options) => {
95
+ if (options.submitEditing) {
96
+ // If we want to submit, we must NOT blur before pressing Enter.
97
+ // We let the runner-side bridge handle both Enter and the subsequent blur.
98
+ await window.__RN_HARNESS_BLUR__(options);
99
+ } else {
100
+ // If there is a focused element, blur it directly in the DOM first
101
+ // to trigger local events, then call the runner-side bridge.
102
+ if (
103
+ document.activeElement instanceof HTMLElement ||
104
+ document.activeElement instanceof SVGElement
105
+ ) {
106
+ document.activeElement.blur();
107
+ }
108
+ await window.__RN_HARNESS_BLUR__(options);
109
+ }
110
+ },
111
+ };
112
+
113
+ export default WebHarnessUI;
package/src/harness.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { Platform } from "react-native";
2
+ import { HarnessUIModule } from "./types.js";
3
+
4
+ const getHarnessUI = (): HarnessUIModule => {
5
+ if (Platform.OS === 'web') {
6
+ return require('./WebHarnessUI.js').default;
7
+ }
8
+
9
+ return require('./NativeHarnessUI.js').default;
10
+ }
11
+
12
+ export default getHarnessUI();
package/src/index.ts CHANGED
@@ -15,4 +15,4 @@ export {
15
15
  type ScreenshotResult,
16
16
  } from './screen.js';
17
17
  export { userEvent, type UserEvent } from './userEvent.js';
18
- export type { ViewInfo } from './NativeHarnessUI.js';
18
+ export type { ViewInfo, BoundingBox } from './types.js';