Skip to content

Commit 098abca

Browse files
committed
refactor: conditional compilation
1 parent bff3577 commit 098abca

12 files changed

Lines changed: 388 additions & 181 deletions

File tree

packages/ui/HarnessUI.podspec

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,13 @@ Pod::Spec.new do |s|
1616
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
1717
s.private_header_files = "ios/**/*.h"
1818

19+
# Conditional compilation - exclude opposite implementation based on build configuration
20+
# Debug builds: Exclude release stubs, include full implementation with private APIs
21+
# Release builds: Exclude debug implementation, include only no-op stubs
22+
s.pod_target_xcconfig = {
23+
'EXCLUDED_SOURCE_FILE_NAMES[config=Debug]' => '$(PODS_TARGET_SRCROOT)/ios/HarnessUIRelease.mm $(PODS_TARGET_SRCROOT)/ios/ViewQueryHelperRelease.mm',
24+
'EXCLUDED_SOURCE_FILE_NAMES[config=Release]' => '$(PODS_TARGET_SRCROOT)/ios/HarnessUIDebug.mm $(PODS_TARGET_SRCROOT)/ios/ViewQueryHelperDebug.mm'
25+
}
26+
1927
install_modules_dependencies(s)
2028
end
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.harnessui
2+
3+
import android.os.Handler
4+
import android.os.Looper
5+
import android.os.SystemClock
6+
import android.util.Log
7+
import android.view.MotionEvent
8+
import com.facebook.react.bridge.Arguments
9+
import com.facebook.react.bridge.Promise
10+
import com.facebook.react.bridge.ReactApplicationContext
11+
import com.facebook.react.bridge.UiThreadUtil
12+
import com.facebook.react.bridge.WritableArray
13+
import com.facebook.react.bridge.WritableMap
14+
import java.util.concurrent.CountDownLatch
15+
import java.util.concurrent.TimeUnit
16+
17+
/**
18+
* Debug implementation of UIHelper with full functionality.
19+
* Includes touch simulation and view querying capabilities.
20+
*/
21+
class UIHelperImpl(private val context: ReactApplicationContext) : UIHelper {
22+
23+
companion object {
24+
private const val TAG = "HarnessUI"
25+
private const val TAP_DURATION_MS = 50L // Duration between touch down and up
26+
private const val EVENT_PROCESSING_DELAY_MS = 10L // Delay after touch up for React Native to process the event
27+
}
28+
29+
private val mainHandler = Handler(Looper.getMainLooper())
30+
31+
// =========================================================================
32+
// Touch Simulation
33+
// =========================================================================
34+
35+
override fun simulateTap(x: Double, y: Double, promise: Promise) {
36+
Log.i(TAG, "simulateTap called with x:$x y:$y")
37+
38+
UiThreadUtil.runOnUiThread {
39+
val activity = context.currentActivity ?: run {
40+
Log.w(TAG, "No current activity")
41+
promise.resolve(null)
42+
return@runOnUiThread
43+
}
44+
val root = activity.window.decorView
45+
46+
// Convert DP to PX
47+
val density = root.resources.displayMetrics.density
48+
val pxX = (x * density).toFloat()
49+
val pxY = (y * density).toFloat()
50+
51+
val downTime = SystemClock.uptimeMillis()
52+
53+
// 1. ACTION_DOWN
54+
val downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, pxX, pxY, 0)
55+
try {
56+
root.dispatchTouchEvent(downEvent)
57+
Log.i(TAG, "Sent touch down at ($pxX, $pxY)")
58+
} finally {
59+
downEvent.recycle()
60+
}
61+
62+
// 2. ACTION_UP after real delay to allow press feedback to render
63+
mainHandler.postDelayed({
64+
val upTime = SystemClock.uptimeMillis()
65+
val upEvent = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, pxX, pxY, 0)
66+
try {
67+
root.dispatchTouchEvent(upEvent)
68+
Log.i(TAG, "Tap completed at ($pxX, $pxY)")
69+
} finally {
70+
upEvent.recycle()
71+
}
72+
// Wait for React Native to process the touch event and trigger JS callbacks
73+
mainHandler.postDelayed({
74+
promise.resolve(null)
75+
}, EVENT_PROCESSING_DELAY_MS)
76+
}, TAP_DURATION_MS)
77+
}
78+
}
79+
80+
// =========================================================================
81+
// Query API
82+
// =========================================================================
83+
84+
override fun queryByTestId(testId: String): WritableMap? {
85+
Log.i(TAG, "queryByTestId called with: $testId")
86+
return executeQuery(ViewQueryType.TEST_ID, testId)
87+
}
88+
89+
override fun queryByAccessibilityLabel(label: String): WritableMap? {
90+
Log.i(TAG, "queryByAccessibilityLabel called with: $label")
91+
return executeQuery(ViewQueryType.ACCESSIBILITY_LABEL, label)
92+
}
93+
94+
override fun queryAllByTestId(testId: String): WritableArray {
95+
Log.i(TAG, "queryAllByTestId called with: $testId")
96+
return executeQueryAll(ViewQueryType.TEST_ID, testId)
97+
}
98+
99+
override fun queryAllByAccessibilityLabel(label: String): WritableArray {
100+
Log.i(TAG, "queryAllByAccessibilityLabel called with: $label")
101+
return executeQueryAll(ViewQueryType.ACCESSIBILITY_LABEL, label)
102+
}
103+
104+
/**
105+
* Executes a query on the UI thread and returns the result.
106+
* Uses CountDownLatch to synchronize with the UI thread.
107+
*/
108+
private fun executeQuery(queryType: ViewQueryType, value: String): WritableMap? {
109+
var result: WritableMap? = null
110+
111+
// If already on UI thread, execute directly
112+
if (UiThreadUtil.isOnUiThread()) {
113+
val activity = context.currentActivity ?: return null
114+
result = ViewQueryHelper.query(activity, queryType, value)?.toWritableMap()
115+
} else {
116+
// Execute on UI thread and wait for result
117+
val latch = CountDownLatch(1)
118+
119+
UiThreadUtil.runOnUiThread {
120+
try {
121+
val activity = context.currentActivity
122+
if (activity != null) {
123+
result = ViewQueryHelper.query(activity, queryType, value)?.toWritableMap()
124+
}
125+
} finally {
126+
latch.countDown()
127+
}
128+
}
129+
130+
// Wait for UI thread with timeout
131+
try {
132+
latch.await(5, TimeUnit.SECONDS)
133+
} catch (e: InterruptedException) {
134+
Log.e(TAG, "Query interrupted", e)
135+
}
136+
}
137+
138+
Log.i(TAG, "Query result: $result")
139+
return result
140+
}
141+
142+
/**
143+
* Executes a query for all matching views on the UI thread.
144+
* Uses CountDownLatch to synchronize with the UI thread.
145+
*/
146+
private fun executeQueryAll(queryType: ViewQueryType, value: String): WritableArray {
147+
var result: WritableArray = Arguments.createArray()
148+
149+
// If already on UI thread, execute directly
150+
if (UiThreadUtil.isOnUiThread()) {
151+
val activity = context.currentActivity ?: return result
152+
val queryResults = ViewQueryHelper.queryAll(activity, queryType, value)
153+
result = Arguments.createArray().apply {
154+
queryResults.forEach { pushMap(it.toWritableMap()) }
155+
}
156+
} else {
157+
// Execute on UI thread and wait for result
158+
val latch = CountDownLatch(1)
159+
160+
UiThreadUtil.runOnUiThread {
161+
try {
162+
val activity = context.currentActivity
163+
if (activity != null) {
164+
val queryResults = ViewQueryHelper.queryAll(activity, queryType, value)
165+
result = Arguments.createArray().apply {
166+
queryResults.forEach { pushMap(it.toWritableMap()) }
167+
}
168+
}
169+
} finally {
170+
latch.countDown()
171+
}
172+
}
173+
174+
// Wait for UI thread with timeout
175+
try {
176+
latch.await(5, TimeUnit.SECONDS)
177+
} catch (e: InterruptedException) {
178+
Log.e(TAG, "QueryAll interrupted", e)
179+
}
180+
}
181+
182+
Log.i(TAG, "QueryAll result count: ${result.size()}")
183+
return result
184+
}
185+
}

packages/ui/android/src/main/java/com/harnessui/ViewQueryHelper.kt renamed to packages/ui/android/src/debug/java/com/harnessui/ViewQueryHelper.kt

File renamed without changes.

0 commit comments

Comments
 (0)