From efd053ada2b49e994d0008302e6ee6d5121a6f1b Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Wed, 27 Sep 2023 15:57:45 +0300 Subject: [PATCH 1/5] fix(android): extract material slider value with reflection. This fixes a `ClassNotFoundException` error that is thrown when the Slider class from the `com.google.android.material.slider` package is not found at runtime. The solution is to use reflection instead of importing the Slider class, which may not necessarily exist in the app. Also, I made a light refactoring for get-attributes related code. --- .../action/AdjustSliderToPositionAction.kt | 4 +- .../espresso/action/GetAttributesAction.kt | 60 +++++++++---------- .../espresso/common/MaterialSliderHelper.kt | 20 +++++++ .../{SliderHelper.kt => ReactSliderHelper.kt} | 11 ++-- .../detox/espresso/matcher/ViewMatchers.kt | 4 +- .../common/MaterialSliderHelperTest.kt | 33 ++++++++++ ...HelperTest.kt => ReactSliderHelperTest.kt} | 6 +- 7 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt rename detox/android/detox/src/full/java/com/wix/detox/espresso/common/{SliderHelper.kt => ReactSliderHelper.kt} (83%) create mode 100644 detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/MaterialSliderHelperTest.kt rename detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/{SliderHelperTest.kt => ReactSliderHelperTest.kt} (90%) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/AdjustSliderToPositionAction.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/AdjustSliderToPositionAction.kt index 3fa8f68ebf..e6562c20b8 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/AdjustSliderToPositionAction.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/AdjustSliderToPositionAction.kt @@ -6,7 +6,7 @@ import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import com.wix.detox.espresso.common.SliderHelper +import com.wix.detox.espresso.common.ReactSliderHelper import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -16,7 +16,7 @@ class AdjustSliderToPositionAction(private val targetPositionPct: Double) : View Matchers.allOf( isDisplayed(), isAssignableFrom(AppCompatSeekBar::class.java) ) override fun perform(uiController: UiController?, view: View) { - val sliderHelper = SliderHelper.create(view) + val sliderHelper = ReactSliderHelper.create(view) sliderHelper.setProgressPct(targetPositionPct) } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt index de70802077..4a591d4bc5 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt @@ -10,7 +10,8 @@ import androidx.test.espresso.UiController import com.google.android.material.slider.Slider import com.wix.detox.espresso.ViewActionWithResult import com.wix.detox.espresso.MultipleViewsAction -import com.wix.detox.espresso.common.SliderHelper +import com.wix.detox.espresso.common.ReactSliderHelper +import com.wix.detox.espresso.common.MaterialSliderHelper import com.wix.detox.reactnative.ui.getAccessibilityLabel import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -18,23 +19,25 @@ import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.notNullValue import org.json.JSONObject +interface AttributeExtractor { + fun extractAttributes(json: JSONObject, view: View) +} + class GetAttributesAction() : ViewActionWithResult, MultipleViewsAction { - private val commonAttributes = CommonAttributes() - private val textViewAttributes = TextViewAttributes() - private val checkBoxAttributes = CheckBoxAttributes() - private val progressBarAttributes = ProgressBarAttributes() - private val sliderAttributes = SliderAttributes() + private val attributeExtractors = listOf( + CommonAttributes(), + TextViewAttributes(), + CheckBoxAttributes(), + ProgressBarAttributes(), + MaterialSliderAttributes() + ) private var result: JSONObject? = null override fun perform(uiController: UiController?, view: View?) { view!! val json = JSONObject() - commonAttributes.get(json, view) - textViewAttributes.get(json, view) - checkBoxAttributes.get(json, view) - progressBarAttributes.get(json, view) - sliderAttributes.get(json, view) + attributeExtractors.forEach { it.extractAttributes(json, view) } result = json } @@ -44,8 +47,8 @@ class GetAttributesAction() : ViewActionWithResult, MultipleViewsAc override fun getConstraints(): Matcher = allOf(notNullValue(), Matchers.isA(View::class.java)) } -private class CommonAttributes { - fun get(json: JSONObject, view: View) { +private class CommonAttributes : AttributeExtractor { + override fun extractAttributes(json: JSONObject, view: View) { getId(json, view) getVisibility(json, view) getAccessibilityLabel(json, view) @@ -89,8 +92,8 @@ private class CommonAttributes { } } -private class TextViewAttributes { - fun get(json: JSONObject, view: View) { +private class TextViewAttributes : AttributeExtractor { + override fun extractAttributes(json: JSONObject, view: View) { if (view is TextView) { getText(json, view) getLength(json, view) @@ -118,8 +121,8 @@ private class TextViewAttributes { } } -private class CheckBoxAttributes { - fun get(json: JSONObject, view: View) { +private class CheckBoxAttributes : AttributeExtractor { + override fun extractAttributes(json: JSONObject, view: View) { if (view is CheckBox) { getCheckboxValue(json, view) } @@ -133,31 +136,28 @@ private class CheckBoxAttributes { * Note: this applies also to [androidx.appcompat.widget.AppCompatSeekBar], which * is anything RN-slider-ish. */ -private class ProgressBarAttributes { - fun get(json: JSONObject, view: View) { +private class ProgressBarAttributes : AttributeExtractor { + override fun extractAttributes(json: JSONObject, view: View) { if (view is ProgressBar) { - SliderHelper.maybeCreate(view)?.let { - getRNSliderValue(json, it) + ReactSliderHelper.maybeCreate(view)?.let { + getReactSliderValue(json, it) } ?: getProgressBarValue(json, view) } } - private fun getRNSliderValue(rootObject: JSONObject, sliderHelper: SliderHelper) { - rootObject.put("value", sliderHelper.getCurrentProgressPct()) + private fun getReactSliderValue(rootObject: JSONObject, reactSliderHelper: ReactSliderHelper) { + rootObject.put("value", reactSliderHelper.getCurrentProgressPct()) } private fun getProgressBarValue(rootObject: JSONObject, view: ProgressBar) = rootObject.put("value", view.progress) } -private class SliderAttributes { - fun get(json: JSONObject, view: View) { - if (view is Slider) { - getSliderValue(json, view) +private class MaterialSliderAttributes : AttributeExtractor { + override fun extractAttributes(json: JSONObject, view: View) { + MaterialSliderHelper(view).getValueIfSlider()?.run { + json.put("value", this) } } - - private fun getSliderValue(rootObject: JSONObject, view: Slider) = - rootObject.put("value", view.value) } diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt new file mode 100644 index 0000000000..c68f670e60 --- /dev/null +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt @@ -0,0 +1,20 @@ +package com.wix.detox.espresso.common + +import android.view.View +import com.wix.detox.espresso.action.common.ReflectUtils +import org.joor.Reflect + +private const val CLASS_MATERIAL_SLIDER = "com.google.android.material.slider.Slider" + +open class MaterialSliderHelper(protected val view: View) { + fun getValueIfSlider(): Double? { + if (!isSlider()) { + null + } + return getValue() + } + + private fun isSlider() = ReflectUtils.isAssignableFrom(view, CLASS_MATERIAL_SLIDER) + + private fun getValue() = Reflect.on(view).call("getValue").get() as Double +} diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/SliderHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/ReactSliderHelper.kt similarity index 83% rename from detox/android/detox/src/full/java/com/wix/detox/espresso/common/SliderHelper.kt rename to detox/android/detox/src/full/java/com/wix/detox/espresso/common/ReactSliderHelper.kt index 6bd4080a08..8da0ebbfa5 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/SliderHelper.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/ReactSliderHelper.kt @@ -11,8 +11,9 @@ import org.joor.Reflect private const val CLASS_REACT_SLIDER_LEGACY = "com.facebook.react.views.slider.ReactSlider" private const val CLASS_REACT_SLIDER_COMMUNITY = "com.reactnativecommunity.slider.ReactSlider" +private const val CLASS_REACT_SLIDER_COMMUNITY_MANAGER = "com.reactnativecommunity.slider.ReactSliderManager" -abstract class SliderHelper(protected val slider: AppCompatSeekBar) { +abstract class ReactSliderHelper(protected val slider: AppCompatSeekBar) { fun getCurrentProgressPct(): Double { val nativeProgress = slider.progress.toDouble() val nativeMax = slider.max @@ -46,7 +47,7 @@ abstract class SliderHelper(protected val slider: AppCompatSeekBar) { ?: throw DetoxIllegalStateException("Cannot handle this type of a seek-bar view (Class ${view.javaClass.canonicalName}). " + "Only React Native sliders are currently supported.") - fun maybeCreate(view: View): SliderHelper? = + fun maybeCreate(view: View): ReactSliderHelper? = when { ReflectUtils.isAssignableFrom(view, CLASS_REACT_SLIDER_LEGACY) -> LegacySliderHelper(view as ReactSlider) @@ -58,7 +59,7 @@ abstract class SliderHelper(protected val slider: AppCompatSeekBar) { } } -private class LegacySliderHelper(slider: ReactSlider): SliderHelper(slider) { +private class LegacySliderHelper(slider: ReactSlider): ReactSliderHelper(slider) { override fun setProgressJS(valueJS: Double) { val reactSliderManager = com.facebook.react.views.slider.ReactSliderManager() reactSliderManager.updateProperties(slider as ReactSlider, buildStyles("value", valueJS)) @@ -67,9 +68,9 @@ private class LegacySliderHelper(slider: ReactSlider): SliderHelper(slider) { private fun buildStyles(vararg keysAndValues: Any) = ReactStylesDiffMap(JavaOnlyMap.of(*keysAndValues)) } -private class CommunitySliderHelper(slider: AppCompatSeekBar): SliderHelper(slider) { +private class CommunitySliderHelper(slider: AppCompatSeekBar): ReactSliderHelper(slider) { override fun setProgressJS(valueJS: Double) { - val reactSliderManager = Class.forName("com.reactnativecommunity.slider.ReactSliderManager").newInstance() + val reactSliderManager = Class.forName(CLASS_REACT_SLIDER_COMMUNITY_MANAGER).newInstance() Reflect.on(reactSliderManager).call("setValue", slider, valueJS) } } diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt index 06b146c531..a4380bf102 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/matcher/ViewMatchers.kt @@ -7,7 +7,7 @@ import androidx.appcompat.widget.AppCompatSeekBar import androidx.test.espresso.matcher.BoundedMatcher import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility -import com.wix.detox.espresso.common.SliderHelper +import com.wix.detox.espresso.common.ReactSliderHelper import org.hamcrest.* import org.hamcrest.Matchers.* import kotlin.math.abs @@ -60,7 +60,7 @@ fun toHaveSliderPosition(expectedValuePct: Double, tolerance: Double): Matcher Date: Wed, 27 Sep 2023 16:40:32 +0300 Subject: [PATCH 2/5] fix(MaterialSliderHelper): return null value if view is not a slider. --- .../java/com/wix/detox/espresso/common/MaterialSliderHelper.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt index c68f670e60..8ba28680db 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt @@ -9,8 +9,9 @@ private const val CLASS_MATERIAL_SLIDER = "com.google.android.material.slider.Sl open class MaterialSliderHelper(protected val view: View) { fun getValueIfSlider(): Double? { if (!isSlider()) { - null + return null } + return getValue() } From 8881a1f485ca3498c06a3da227d1762e5706be4a Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Wed, 27 Sep 2023 16:44:27 +0300 Subject: [PATCH 3/5] fix(MaterialSliderHelper): return float value, not double. --- .../com/wix/detox/espresso/common/MaterialSliderHelper.kt | 4 ++-- .../com/wix/detox/espresso/common/MaterialSliderHelperTest.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt index 8ba28680db..479ac09c82 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/common/MaterialSliderHelper.kt @@ -7,7 +7,7 @@ import org.joor.Reflect private const val CLASS_MATERIAL_SLIDER = "com.google.android.material.slider.Slider" open class MaterialSliderHelper(protected val view: View) { - fun getValueIfSlider(): Double? { + fun getValueIfSlider(): Float? { if (!isSlider()) { return null } @@ -17,5 +17,5 @@ open class MaterialSliderHelper(protected val view: View) { private fun isSlider() = ReflectUtils.isAssignableFrom(view, CLASS_MATERIAL_SLIDER) - private fun getValue() = Reflect.on(view).call("getValue").get() as Double + private fun getValue() = Reflect.on(view).call("getValue").get() as Float } diff --git a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/MaterialSliderHelperTest.kt b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/MaterialSliderHelperTest.kt index ba60b2ea0a..8466ec4bc0 100644 --- a/detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/MaterialSliderHelperTest.kt +++ b/detox/android/detox/src/testFull/java/com/wix/detox/espresso/common/MaterialSliderHelperTest.kt @@ -19,7 +19,7 @@ class MaterialSliderHelperTest { val uut = MaterialSliderHelper(slider) - assertThat(uut.getValueIfSlider()).isEqualTo(0.2) + assertThat(uut.getValueIfSlider()).isEqualTo(0.2f) } @Test From 1d9a85c53d374df16cdf4eb569feadd2f89c495a Mon Sep 17 00:00:00 2001 From: Asaf Korem Date: Wed, 27 Sep 2023 17:02:05 +0300 Subject: [PATCH 4/5] build(android): import material package as test-implementation. --- detox/android/detox/build.gradle | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/detox/android/detox/build.gradle b/detox/android/detox/build.gradle index b6e5611053..69cf1a0ea7 100644 --- a/detox/android/detox/build.gradle +++ b/detox/android/detox/build.gradle @@ -126,9 +126,6 @@ dependencies { // Third-party/extension deps. dependencies { - implementation("com.google.android.material:material:$_materialMinVersion") { - because 'Material components are mentioned explicitly (e.g. Slider in get-attributes handler)' - } implementation('org.apache.commons:commons-lang3:3.7') { because 'Needed by invoke. Warning: Upgrading to newer versions is not seamless.' } @@ -150,6 +147,10 @@ dependencies { testImplementation 'org.apache.commons:commons-io:1.3.2' testImplementation 'org.mockito.kotlin:mockito-kotlin:4.0.0' testImplementation 'org.robolectric:robolectric:4.4' + + testImplementation("com.google.android.material:material:$_materialMinVersion") { + because 'Material components are mentioned explicitly (e.g. Slider in get-attributes handler)' + } } // Spek (https://spekframework.org/setup-android) From 4150806ea3c009a8611bc84dd69d868284cdba14 Mon Sep 17 00:00:00 2001 From: Asaf Korem <55082339+asafkorem@users.noreply.github.com> Date: Wed, 27 Sep 2023 17:03:04 +0300 Subject: [PATCH 5/5] Apply suggestions from code review --- .../com/wix/detox/espresso/action/GetAttributesAction.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt index 4a591d4bc5..a159fbf924 100644 --- a/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt +++ b/detox/android/detox/src/full/java/com/wix/detox/espresso/action/GetAttributesAction.kt @@ -7,7 +7,6 @@ import android.widget.CheckBox import android.widget.ProgressBar import android.widget.TextView import androidx.test.espresso.UiController -import com.google.android.material.slider.Slider import com.wix.detox.espresso.ViewActionWithResult import com.wix.detox.espresso.MultipleViewsAction import com.wix.detox.espresso.common.ReactSliderHelper @@ -19,7 +18,7 @@ import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.notNullValue import org.json.JSONObject -interface AttributeExtractor { +private interface AttributeExtractor { fun extractAttributes(json: JSONObject, view: View) } @@ -156,8 +155,8 @@ private class ProgressBarAttributes : AttributeExtractor { private class MaterialSliderAttributes : AttributeExtractor { override fun extractAttributes(json: JSONObject, view: View) { - MaterialSliderHelper(view).getValueIfSlider()?.run { - json.put("value", this) + MaterialSliderHelper(view).getValueIfSlider()?.let { + json.put("value", it) } } }