Skip to content

Commit

Permalink
Merge pull request #4203 from wix/slider-get-attributes
Browse files Browse the repository at this point in the history
fix(android): extract material slider value attribute with reflection.
  • Loading branch information
asafkorem authored Sep 27, 2023
2 parents 133cf7c + 4150806 commit 446bc41
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 46 deletions.
7 changes: 4 additions & 3 deletions detox/android/detox/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,36 @@ 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.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
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.notNullValue
import org.json.JSONObject

private interface AttributeExtractor {
fun extractAttributes(json: JSONObject, view: View)
}

class GetAttributesAction() : ViewActionWithResult<JSONObject?>, 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
}
Expand All @@ -44,8 +46,8 @@ class GetAttributesAction() : ViewActionWithResult<JSONObject?>, MultipleViewsAc
override fun getConstraints(): Matcher<View> = 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)
Expand Down Expand Up @@ -89,8 +91,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)
Expand Down Expand Up @@ -118,8 +120,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)
}
Expand All @@ -133,31 +135,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()?.let {
json.put("value", it)
}
}

private fun getSliderValue(rootObject: JSONObject, view: Slider) =
rootObject.put("value", view.value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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(): Float? {
if (!isSlider()) {
return null
}

return getValue()
}

private fun isSlider() = ReflectUtils.isAssignableFrom(view, CLASS_MATERIAL_SLIDER)

private fun getValue() = Reflect.on(view).call("getValue").get() as Float
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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))
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -60,7 +60,7 @@ fun toHaveSliderPosition(expectedValuePct: Double, tolerance: Double): Matcher<V
}

override fun matchesSafely(view: AppCompatSeekBar): Boolean {
val sliderHelper = SliderHelper.create(view)
val sliderHelper = ReactSliderHelper.create(view)
val progressPct = sliderHelper.getCurrentProgressPct()
return (abs(progressPct - expectedValuePct) <= tolerance)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.wix.detox.espresso.common

import android.view.View
import com.google.android.material.slider.Slider
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class MaterialSliderHelperTest {
@Test
fun `should return value if view is a slider`() {
val slider: Slider = mock {
on { value } doReturn 0.2f
}

val uut = MaterialSliderHelper(slider)

assertThat(uut.getValueIfSlider()).isEqualTo(0.2f)
}

@Test
fun `should return null if view is not a slider`() {
val view: View = mock()

val uut = MaterialSliderHelper(view)

assertThat(uut.getValueIfSlider()).isNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import org.robolectric.RobolectricTestRunner
* to avoid having to install the community slider under node_modules just for this.
*/
@RunWith(RobolectricTestRunner::class)
class SliderHelperTest {
class ReactSliderHelperTest {
lateinit var slider: ReactSlider
lateinit var uut: SliderHelper
lateinit var uut: ReactSliderHelper

@Before
fun setup() {
slider = mock()
uut = SliderHelper.create(slider)
uut = ReactSliderHelper.create(slider)
}

private fun givenNativeProgressTraits(current: Int, max: Int) {
Expand Down

0 comments on commit 446bc41

Please sign in to comment.