@react-native-harness/ui 1.0.0-alpha.24 → 1.0.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/README.md CHANGED
@@ -13,7 +13,7 @@ Native UI testing module for React Native Harness that provides view queries and
13
13
 
14
14
  - **View Queries**: Find elements by testID or accessibility label
15
15
  - **Touch Simulation**: Simulate user presses and text input
16
- - **Screenshot Capture**: Capture screenshots of the entire screen or specific elements
16
+ - **Screenshot Capture**: Capture screenshots of the entire screen, specific elements, or custom regions
17
17
  - **Debug-Only**: Automatically excluded from release builds, only available in debug builds
18
18
 
19
19
  ## Installation
@@ -45,6 +45,14 @@ describe('My Component', () => {
45
45
 
46
46
  // Take screenshots for debugging
47
47
  const screenshot = await screen.screenshot();
48
+
49
+ // Or capture a specific region
50
+ const regionScreenshot = await screen.screenshot({
51
+ x: 0,
52
+ y: 0,
53
+ width: 100,
54
+ height: 100,
55
+ });
48
56
  });
49
57
  });
50
58
  ```
@@ -88,11 +96,13 @@ Queries for an element by accessibility label without throwing. Returns null if
88
96
 
89
97
  Queries for all elements by accessibility label without throwing. Returns an empty array if none found.
90
98
 
91
- #### `screenshot(element?: ElementReference): Promise<ScreenshotResult | null>`
99
+ #### `screenshot(target?: ElementReference | BoundingBox): Promise<ScreenshotResult | null>`
92
100
 
93
- Captures a screenshot of the entire app window or a specific element.
101
+ Captures a screenshot of the entire app window, a specific element, or a custom region.
94
102
  Returns a ScreenshotResult with PNG data, or null if capture fails.
95
103
 
104
+ > **Warning**: If you are capturing screenshots of elements that extend beyond the screen boundaries (e.g., large scroll views or absolutely positioned views that are partially off-screen), you must disable view flattening in your configuration by setting `disableViewFlattening: true` in your `rn-harness.config.mjs` file.
105
+
96
106
  ### `userEvent`
97
107
 
98
108
  Provides methods to simulate user interactions.
@@ -110,6 +120,7 @@ Simulates a press at the specified screen coordinates.
110
120
  Simulates typing text into a text input element. Focuses the element, types each character, and blurs the element.
111
121
 
112
122
  **TypeOptions:**
123
+
113
124
  - `skipPress?: boolean` - If true, pressIn and pressOut events will not be triggered
114
125
  - `skipBlur?: boolean` - If true, endEditing and blur events will not be triggered
115
126
  - `submitEditing?: boolean` - If true, submitEditing event will be triggered after typing
@@ -118,16 +129,19 @@ Simulates typing text into a text input element. Focuses the element, types each
118
129
 
119
130
  ### `ElementReference`
120
131
 
121
- Represents an element found on screen with its position and dimensions.
132
+ An opaque reference to an element found on screen.
133
+
134
+ ### `BoundingBox`
135
+
136
+ Represents a region on screen.
122
137
 
123
138
  ```typescript
124
- type ElementReference = {
139
+ interface BoundingBox {
125
140
  x: number;
126
141
  y: number;
127
142
  width: number;
128
143
  height: number;
129
- // ... additional view info
130
- };
144
+ }
131
145
  ```
132
146
 
133
147
  ### `ScreenshotResult`
@@ -136,9 +150,9 @@ Screenshot result containing PNG image data.
136
150
 
137
151
  ```typescript
138
152
  interface ScreenshotResult {
139
- data: Uint8Array; // PNG image data
140
- width: number; // Width in logical pixels
141
- height: number; // Height in logical pixels
153
+ data: Uint8Array; // PNG image data
154
+ width: number; // Width in logical pixels
155
+ height: number; // Height in logical pixels
142
156
  }
143
157
  ```
144
158
 
@@ -161,4 +175,4 @@ Like the project? ⚛️ [Join the team](https://callstack.com/careers/?utm_camp
161
175
  [prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
162
176
  [prs-welcome]: ../../CONTRIBUTING.md
163
177
  [chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
164
- [chat]: https://discord.gg/xgGt7KAjxv
178
+ [chat]: https://discord.gg/xgGt7KAjxv
@@ -12,9 +12,9 @@ import com.facebook.react.module.annotations.ReactModule
12
12
  * Includes touch simulation and view querying.
13
13
  */
14
14
  @ReactModule(name = HarnessUIModule.NAME)
15
- class HarnessUIModule(reactContext: ReactApplicationContext) :
16
- NativeHarnessUISpec(reactContext) {
17
-
15
+ class HarnessUIModule(
16
+ reactContext: ReactApplicationContext,
17
+ ) : NativeHarnessUISpec(reactContext) {
18
18
  companion object {
19
19
  const val NAME = "HarnessUI"
20
20
  }
@@ -23,31 +23,41 @@ class HarnessUIModule(reactContext: ReactApplicationContext) :
23
23
 
24
24
  override fun getName(): String = NAME
25
25
 
26
- override fun simulatePress(x: Double, y: Double, promise: Promise) {
27
- helper.simulatePress(x, y, promise)
26
+ override fun simulatePress(
27
+ nativeId: String,
28
+ x: Double,
29
+ y: Double,
30
+ promise: Promise,
31
+ ) {
32
+ helper.simulatePress(nativeId, x, y, promise)
28
33
  }
29
34
 
30
- override fun queryByTestId(testId: String): WritableMap? =
31
- helper.queryByTestId(testId)
35
+ override fun queryByTestId(testId: String): WritableMap? = helper.queryByTestId(testId)
32
36
 
33
- override fun queryByAccessibilityLabel(label: String): WritableMap? =
34
- helper.queryByAccessibilityLabel(label)
37
+ override fun queryByAccessibilityLabel(label: String): WritableMap? = helper.queryByAccessibilityLabel(label)
35
38
 
36
- override fun queryAllByTestId(testId: String): WritableArray =
37
- helper.queryAllByTestId(testId)
39
+ override fun queryAllByTestId(testId: String): WritableArray = helper.queryAllByTestId(testId)
38
40
 
39
- override fun queryAllByAccessibilityLabel(label: String): WritableArray =
40
- helper.queryAllByAccessibilityLabel(label)
41
+ override fun queryAllByAccessibilityLabel(label: String): WritableArray = helper.queryAllByAccessibilityLabel(label)
41
42
 
42
- override fun captureScreenshot(bounds: ReadableMap?, promise: Promise) {
43
+ override fun captureScreenshot(
44
+ bounds: ReadableMap?,
45
+ promise: Promise,
46
+ ) {
43
47
  helper.captureScreenshot(bounds, promise)
44
48
  }
45
49
 
46
- override fun typeChar(character: String, promise: Promise) {
50
+ override fun typeChar(
51
+ character: String,
52
+ promise: Promise,
53
+ ) {
47
54
  helper.typeChar(character, promise)
48
55
  }
49
56
 
50
- override fun blur(options: ReadableMap, promise: Promise) {
57
+ override fun blur(
58
+ options: ReadableMap,
59
+ promise: Promise,
60
+ ) {
51
61
  helper.blur(options, promise)
52
62
  }
53
63
  }
@@ -1,7 +1,9 @@
1
1
  package com.harnessui
2
2
 
3
+ import android.content.Context
3
4
  import android.graphics.Bitmap
4
5
  import android.graphics.Canvas
6
+ import android.graphics.Rect
5
7
  import android.os.Handler
6
8
  import android.os.Looper
7
9
  import android.os.SystemClock
@@ -9,7 +11,6 @@ import android.util.Log
9
11
  import android.view.MotionEvent
10
12
  import android.view.inputmethod.EditorInfo
11
13
  import android.view.inputmethod.InputMethodManager
12
- import android.content.Context
13
14
  import android.widget.EditText
14
15
  import com.facebook.react.bridge.Arguments
15
16
  import com.facebook.react.bridge.Promise
@@ -28,12 +29,13 @@ import java.util.concurrent.TimeUnit
28
29
  * UI helper implementation for HarnessUI.
29
30
  * Includes touch simulation and view querying capabilities.
30
31
  */
31
- class UIHelperImpl(private val context: ReactApplicationContext) {
32
-
32
+ class UIHelperImpl(
33
+ private val context: ReactApplicationContext,
34
+ ) {
33
35
  companion object {
34
36
  private const val TAG = "HarnessUI"
35
- private const val TAP_DURATION_MS = 50L // Duration between touch down and up
36
- private const val EVENT_PROCESSING_DELAY_MS = 10L // Delay after touch up for React Native to process the event
37
+ private const val TAP_DURATION_MS = 50L // Duration between touch down and up
38
+ private const val EVENT_PROCESSING_DELAY_MS = 10L // Delay after touch up for React Native to process the event
37
39
  }
38
40
 
39
41
  private val mainHandler = Handler(Looper.getMainLooper())
@@ -42,21 +44,50 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
42
44
  // Touch Simulation
43
45
  // =========================================================================
44
46
 
45
- fun simulatePress(x: Double, y: Double, promise: Promise) {
46
- Log.i(TAG, "simulatePress called with x:$x y:$y")
47
+ fun simulatePress(
48
+ nativeId: String,
49
+ x: Double,
50
+ y: Double,
51
+ promise: Promise,
52
+ ) {
53
+ Log.i(TAG, "simulatePress called with nativeId:$nativeId x:$x y:$y")
47
54
 
48
55
  UiThreadUtil.runOnUiThread {
49
- val activity = context.currentActivity ?: run {
50
- Log.w(TAG, "No current activity")
51
- promise.resolve(null)
52
- return@runOnUiThread
53
- }
56
+ val activity =
57
+ context.currentActivity ?: run {
58
+ Log.w(TAG, "No current activity")
59
+ promise.resolve(null)
60
+ return@runOnUiThread
61
+ }
54
62
  val root = activity.window.decorView
63
+ val density = root.resources.displayMetrics.density
64
+
65
+ var targetX = x
66
+ var targetY = y
67
+
68
+ // If nativeId is provided, try to find the view and get its current position
69
+ if (nativeId.isNotEmpty()) {
70
+ val targetView = ViewRegistry.get(nativeId)
71
+ if (targetView != null) {
72
+ val location = IntArray(2)
73
+ targetView.getLocationOnScreen(location)
74
+ // Calculate center in DP
75
+ val viewX = location[0] / density
76
+ val viewY = location[1] / density
77
+ val viewW = targetView.width / density
78
+ val viewH = targetView.height / density
79
+
80
+ targetX = (viewX + viewW / 2.0)
81
+ targetY = (viewY + viewH / 2.0)
82
+ Log.i(TAG, "Resolved view $nativeId to coordinates ($targetX, $targetY)")
83
+ } else {
84
+ Log.w(TAG, "View with nativeId $nativeId not found or collected. Using provided coordinates.")
85
+ }
86
+ }
55
87
 
56
88
  // Convert DP to PX
57
- val density = root.resources.displayMetrics.density
58
- val pxX = (x * density).toFloat()
59
- val pxY = (y * density).toFloat()
89
+ val pxX = (targetX * density).toFloat()
90
+ val pxY = (targetY * density).toFloat()
60
91
 
61
92
  val downTime = SystemClock.uptimeMillis()
62
93
 
@@ -115,7 +146,10 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
115
146
  * Executes a query on the UI thread and returns the result.
116
147
  * Uses CountDownLatch to synchronize with the UI thread.
117
148
  */
118
- private fun executeQuery(queryType: ViewQueryType, value: String): WritableMap? {
149
+ private fun executeQuery(
150
+ queryType: ViewQueryType,
151
+ value: String,
152
+ ): WritableMap? {
119
153
  var result: WritableMap? = null
120
154
 
121
155
  // If already on UI thread, execute directly
@@ -153,16 +187,20 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
153
187
  * Executes a query for all matching views on the UI thread.
154
188
  * Uses CountDownLatch to synchronize with the UI thread.
155
189
  */
156
- private fun executeQueryAll(queryType: ViewQueryType, value: String): WritableArray {
190
+ private fun executeQueryAll(
191
+ queryType: ViewQueryType,
192
+ value: String,
193
+ ): WritableArray {
157
194
  var result: WritableArray = Arguments.createArray()
158
195
 
159
196
  // If already on UI thread, execute directly
160
197
  if (UiThreadUtil.isOnUiThread()) {
161
198
  val activity = context.currentActivity ?: return result
162
199
  val queryResults = ViewQueryHelper.queryAll(activity, queryType, value)
163
- result = Arguments.createArray().apply {
164
- queryResults.forEach { pushMap(it.toWritableMap()) }
165
- }
200
+ result =
201
+ Arguments.createArray().apply {
202
+ queryResults.forEach { pushMap(it.toWritableMap()) }
203
+ }
166
204
  } else {
167
205
  // Execute on UI thread and wait for result
168
206
  val latch = CountDownLatch(1)
@@ -172,9 +210,10 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
172
210
  val activity = context.currentActivity
173
211
  if (activity != null) {
174
212
  val queryResults = ViewQueryHelper.queryAll(activity, queryType, value)
175
- result = Arguments.createArray().apply {
176
- queryResults.forEach { pushMap(it.toWritableMap()) }
177
- }
213
+ result =
214
+ Arguments.createArray().apply {
215
+ queryResults.forEach { pushMap(it.toWritableMap()) }
216
+ }
178
217
  }
179
218
  } finally {
180
219
  latch.countDown()
@@ -197,20 +236,68 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
197
236
  // Screenshot Capture
198
237
  // =========================================================================
199
238
 
200
- fun captureScreenshot(bounds: ReadableMap?, promise: Promise) {
239
+ fun captureScreenshot(
240
+ bounds: ReadableMap?,
241
+ promise: Promise,
242
+ ) {
201
243
  Log.i(TAG, "captureScreenshot called")
202
244
 
203
245
  UiThreadUtil.runOnUiThread {
204
- val activity = context.currentActivity ?: run {
205
- Log.w(TAG, "No current activity")
206
- promise.resolve(null)
207
- return@runOnUiThread
208
- }
246
+ val activity =
247
+ context.currentActivity ?: run {
248
+ Log.w(TAG, "No current activity")
249
+ promise.resolve(null)
250
+ return@runOnUiThread
251
+ }
209
252
 
210
253
  val root = activity.window.decorView.rootView
211
254
  val density = root.resources.displayMetrics.density
212
255
 
213
256
  try {
257
+ // Check if we have a specific view to capture via nativeId
258
+ val nativeId = if (bounds != null && bounds.hasKey("nativeId")) bounds.getString("nativeId") else null
259
+
260
+ if (!nativeId.isNullOrEmpty()) {
261
+ val targetView = ViewRegistry.get(nativeId)
262
+ if (targetView != null) {
263
+ val width = targetView.width
264
+ val height = targetView.height
265
+
266
+ if (width > 0 && height > 0) {
267
+ val location = IntArray(2)
268
+ targetView.getLocationOnScreen(location)
269
+ val viewRect = Rect(location[0], location[1], location[0] + width, location[1] + height)
270
+ val rootRect = Rect(0, 0, root.width, root.height)
271
+
272
+ val isFullyOnScreen = rootRect.contains(viewRect)
273
+ val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
274
+ val canvas = Canvas(bitmap)
275
+
276
+ if (isFullyOnScreen) {
277
+ Log.i(TAG, "View $nativeId is fully on screen. Using screen-based screenshotting.")
278
+ // Translate canvas to capture the view from the root view hierarchy
279
+ canvas.translate(-location[0].toFloat(), -location[1].toFloat())
280
+ root.draw(canvas)
281
+ } else {
282
+ Log.i(TAG, "View $nativeId is partially/fully off screen. Using direct view render.")
283
+ // Draw the specific view directly
284
+ targetView.draw(canvas)
285
+ }
286
+
287
+ val outputStream = ByteArrayOutputStream()
288
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
289
+ val pngBytes = outputStream.toByteArray()
290
+ bitmap.recycle()
291
+
292
+ val base64String = android.util.Base64.encodeToString(pngBytes, android.util.Base64.NO_WRAP)
293
+ promise.resolve(base64String)
294
+ return@runOnUiThread
295
+ }
296
+ } else {
297
+ Log.w(TAG, "View with nativeId $nativeId not found for screenshot, falling back to window screenshot")
298
+ }
299
+ }
300
+
214
301
  // Determine capture dimensions
215
302
  val captureX: Int
216
303
  val captureY: Int
@@ -268,15 +355,19 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
268
355
  // Text Input
269
356
  // =========================================================================
270
357
 
271
- fun typeChar(character: String, promise: Promise) {
358
+ fun typeChar(
359
+ character: String,
360
+ promise: Promise,
361
+ ) {
272
362
  Log.i(TAG, "typeChar called with: $character")
273
363
 
274
364
  UiThreadUtil.runOnUiThread {
275
- val activity = context.currentActivity ?: run {
276
- Log.w(TAG, "No current activity")
277
- promise.resolve(null)
278
- return@runOnUiThread
279
- }
365
+ val activity =
366
+ context.currentActivity ?: run {
367
+ Log.w(TAG, "No current activity")
368
+ promise.resolve(null)
369
+ return@runOnUiThread
370
+ }
280
371
 
281
372
  val focused = activity.currentFocus
282
373
  if (focused is EditText) {
@@ -296,16 +387,20 @@ class UIHelperImpl(private val context: ReactApplicationContext) {
296
387
  }
297
388
  }
298
389
 
299
- fun blur(options: ReadableMap, promise: Promise) {
390
+ fun blur(
391
+ options: ReadableMap,
392
+ promise: Promise,
393
+ ) {
300
394
  val submitEditing = options.getBoolean("submitEditing")
301
395
  Log.i(TAG, "blur called, submitEditing: $submitEditing")
302
396
 
303
397
  UiThreadUtil.runOnUiThread {
304
- val activity = context.currentActivity ?: run {
305
- Log.w(TAG, "No current activity")
306
- promise.resolve(null)
307
- return@runOnUiThread
308
- }
398
+ val activity =
399
+ context.currentActivity ?: run {
400
+ Log.w(TAG, "No current activity")
401
+ promise.resolve(null)
402
+ return@runOnUiThread
403
+ }
309
404
 
310
405
  val focused = activity.currentFocus
311
406
 
@@ -11,7 +11,7 @@ import com.facebook.react.bridge.WritableMap
11
11
  */
12
12
  enum class ViewQueryType {
13
13
  TEST_ID,
14
- ACCESSIBILITY_LABEL
14
+ ACCESSIBILITY_LABEL,
15
15
  }
16
16
 
17
17
  /**
@@ -21,16 +21,17 @@ data class ViewQueryResult(
21
21
  val x: Float,
22
22
  val y: Float,
23
23
  val width: Float,
24
- val height: Float
24
+ val height: Float,
25
+ val nativeId: String,
25
26
  ) {
26
- fun toWritableMap(): WritableMap {
27
- return Arguments.createMap().apply {
27
+ fun toWritableMap(): WritableMap =
28
+ Arguments.createMap().apply {
28
29
  putDouble("x", x.toDouble())
29
30
  putDouble("y", y.toDouble())
30
31
  putDouble("width", width.toDouble())
31
32
  putDouble("height", height.toDouble())
33
+ putString("nativeId", nativeId)
32
34
  }
33
- }
34
35
  }
35
36
 
36
37
  /**
@@ -38,7 +39,6 @@ data class ViewQueryResult(
38
39
  * Provides reusable query logic for finding views by various criteria.
39
40
  */
40
41
  object ViewQueryHelper {
41
-
42
42
  /**
43
43
  * Finds the first view matching the query criteria.
44
44
  * @param activity The current activity.
@@ -46,7 +46,11 @@ object ViewQueryHelper {
46
46
  * @param value The value to match against.
47
47
  * @return ViewQueryResult if found, null otherwise.
48
48
  */
49
- fun query(activity: Activity, queryType: ViewQueryType, value: String): ViewQueryResult? {
49
+ fun query(
50
+ activity: Activity,
51
+ queryType: ViewQueryType,
52
+ value: String,
53
+ ): ViewQueryResult? {
50
54
  val root = activity.window.decorView
51
55
  val density = root.resources.displayMetrics.density
52
56
  val found = findViewInView(root, queryType, value) ?: return null
@@ -60,7 +64,11 @@ object ViewQueryHelper {
60
64
  * @param value The value to match against.
61
65
  * @return List of ViewQueryResult objects.
62
66
  */
63
- fun queryAll(activity: Activity, queryType: ViewQueryType, value: String): List<ViewQueryResult> {
67
+ fun queryAll(
68
+ activity: Activity,
69
+ queryType: ViewQueryType,
70
+ value: String,
71
+ ): List<ViewQueryResult> {
64
72
  val root = activity.window.decorView
65
73
  val density = root.resources.displayMetrics.density
66
74
  val views = mutableListOf<View>()
@@ -71,17 +79,24 @@ object ViewQueryHelper {
71
79
  /**
72
80
  * Checks if a view matches the given query criteria.
73
81
  */
74
- private fun viewMatches(view: View, queryType: ViewQueryType, value: String): Boolean {
75
- return when (queryType) {
82
+ private fun viewMatches(
83
+ view: View,
84
+ queryType: ViewQueryType,
85
+ value: String,
86
+ ): Boolean =
87
+ when (queryType) {
76
88
  ViewQueryType.TEST_ID -> view.tag == value
77
89
  ViewQueryType.ACCESSIBILITY_LABEL -> view.contentDescription?.toString() == value
78
90
  }
79
- }
80
91
 
81
92
  /**
82
93
  * Recursively finds the first view matching the query criteria.
83
94
  */
84
- private fun findViewInView(view: View, queryType: ViewQueryType, value: String): View? {
95
+ private fun findViewInView(
96
+ view: View,
97
+ queryType: ViewQueryType,
98
+ value: String,
99
+ ): View? {
85
100
  if (viewMatches(view, queryType, value)) {
86
101
  return view
87
102
  }
@@ -104,7 +119,7 @@ object ViewQueryHelper {
104
119
  view: View,
105
120
  queryType: ViewQueryType,
106
121
  value: String,
107
- results: MutableList<View>
122
+ results: MutableList<View>,
108
123
  ) {
109
124
  if (viewMatches(view, queryType, value)) {
110
125
  results.add(view)
@@ -120,7 +135,10 @@ object ViewQueryHelper {
120
135
  /**
121
136
  * Converts a View to a ViewQueryResult with screen coordinates in dp.
122
137
  */
123
- private fun resultFromView(view: View, density: Float): ViewQueryResult {
138
+ private fun resultFromView(
139
+ view: View,
140
+ density: Float,
141
+ ): ViewQueryResult {
124
142
  val location = IntArray(2)
125
143
  view.getLocationOnScreen(location)
126
144
 
@@ -130,6 +148,9 @@ object ViewQueryHelper {
130
148
  val width = view.width / density
131
149
  val height = view.height / density
132
150
 
133
- return ViewQueryResult(x, y, width, height)
151
+ // Register the view and get its ID
152
+ val nativeId = ViewRegistry.register(view)
153
+
154
+ return ViewQueryResult(x, y, width, height, nativeId)
134
155
  }
135
156
  }
@@ -0,0 +1,51 @@
1
+ package com.harnessui
2
+
3
+ import android.view.View
4
+ import java.lang.ref.WeakReference
5
+ import java.util.UUID
6
+ import java.util.concurrent.ConcurrentHashMap
7
+
8
+ /**
9
+ * A thread-safe registry that maps unique string IDs to View instances using WeakReferences.
10
+ * This allows efficient O(1) lookup of Views for actions without keeping them alive (preventing leaks).
11
+ */
12
+ object ViewRegistry {
13
+ private val registry = ConcurrentHashMap<String, WeakReference<View>>()
14
+
15
+ /**
16
+ * Registers a view and returns a unique ID.
17
+ * If the view is already registered (by object identity), it *could* return the existing ID,
18
+ * but for simplicity and performance we can generate a new one or use hash-based key.
19
+ *
20
+ * Currently, we generate a new random ID for every query result to ensure freshness.
21
+ */
22
+ fun register(view: View): String {
23
+ val id = UUID.randomUUID().toString()
24
+ registry[id] = WeakReference(view)
25
+ return id
26
+ }
27
+
28
+ /**
29
+ * Retrieves a view by its ID.
30
+ * Returns null if the ID doesn't exist or the View has been garbage collected.
31
+ */
32
+ fun get(id: String): View? {
33
+ val ref = registry[id] ?: return null
34
+ return ref.get()
35
+ }
36
+
37
+ /**
38
+ * Optional: Clean up stale entries.
39
+ * Since we use WeakReference, the objects are collected, but the keys remain.
40
+ * For a test harness, this memory overhead is usually negligible, but a cleanup
41
+ * could be triggered periodically if needed.
42
+ */
43
+ fun prune() {
44
+ val iterator = registry.entries.iterator()
45
+ while (iterator.hasNext()) {
46
+ if (iterator.next().value.get() == null) {
47
+ iterator.remove()
48
+ }
49
+ }
50
+ }
51
+ }
@@ -1,12 +1,22 @@
1
1
  import { type TurboModule } from 'react-native';
2
- export interface ViewInfo {
2
+ /**
3
+ * Represents a bounding box in screen coordinates (points/dp).
4
+ */
5
+ export interface BoundingBox {
3
6
  x: number;
4
7
  y: number;
5
8
  width: number;
6
9
  height: number;
7
10
  }
11
+ /**
12
+ * Internal interface used for bridge communication.
13
+ * This needs to be exported for TurboModule codegen.
14
+ */
15
+ export interface ViewInfo extends BoundingBox {
16
+ nativeId: string;
17
+ }
8
18
  interface Spec extends TurboModule {
9
- simulatePress(x: number, y: number): Promise<void>;
19
+ simulatePress(nativeId: string, x: number, y: number): Promise<void>;
10
20
  queryByTestId(testId: string): ViewInfo | null;
11
21
  queryAllByTestId(testId: string): ViewInfo[];
12
22
  queryByAccessibilityLabel(label: string): ViewInfo | null;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeHarnessUI.d.ts","sourceRoot":"","sources":["../src/NativeHarnessUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAGrE,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,IAAK,SAAQ,WAAW;IAChC,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IAC7C,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC1D,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACxD,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,OAAO,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;;AAED,wBAAmE"}
1
+ {"version":3,"file":"NativeHarnessUI.d.ts","sourceRoot":"","sources":["../src/NativeHarnessUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,QAAS,SAAQ,WAAW;IAC3C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,IAAK,SAAQ,WAAW;IAChC,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrE,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IAC7C,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC1D,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;IACxD,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,OAAO,EAAE;QAAE,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3D;;AAED,wBAAmE"}
@@ -7,6 +7,7 @@ declare global {
7
7
  __RN_HARNESS_BLUR__: (options: {
8
8
  submitEditing?: boolean;
9
9
  }) => Promise<void>;
10
+ __RN_HARNESS_VIEW_REGISTRY__: Map<string, Element>;
10
11
  }
11
12
  }
12
13
  declare const WebHarnessUI: HarnessUIModule;
@@ -1 +1 @@
1
- {"version":3,"file":"WebHarnessUI.d.ts","sourceRoot":"","sources":["../src/WebHarnessUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,iCAAiC,EAAE,CACjC,MAAM,EAAE,QAAQ,GAAG,IAAI,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QAC5B,6BAA6B,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/D,mBAAmB,EAAE,CAAC,OAAO,EAAE;YAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;SACzB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KACrB;CACF;AAYD,QAAA,MAAM,YAAY,EAAE,eAkDnB,CAAC;AAEF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"WebHarnessUI.d.ts","sourceRoot":"","sources":["../src/WebHarnessUI.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEjE,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,iCAAiC,EAAE,CACjC,MAAM,EAAE,QAAQ,GAAG,IAAI,KACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;QAC5B,6BAA6B,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACvE,wBAAwB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QAC/D,mBAAmB,EAAE,CAAC,OAAO,EAAE;YAC7B,aAAa,CAAC,EAAE,OAAO,CAAC;SACzB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACpB,4BAA4B,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpD;CACF;AA2BD,QAAA,MAAM,YAAY,EAAE,eAqEnB,CAAC;AAEF,eAAe,YAAY,CAAC"}