Added Android Unit and UI tests for DivSelect

This commit is contained in:
vyaivanove
2023-06-05 13:36:53 +03:00
parent d3bbc95c3c
commit 44fbf3ade7
10 changed files with 646 additions and 1 deletions
@@ -2,6 +2,7 @@ package com.yandex.div.core.expression.variables
import androidx.annotation.MainThread
import com.yandex.div.core.Disposable
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.core.dagger.DivScope
import com.yandex.div.core.expression.ExpressionsRuntimeProvider
import com.yandex.div.core.view2.Div2View
@@ -10,6 +11,7 @@ import com.yandex.div.data.Variable
import javax.inject.Inject
@DivScope
@Mockable
internal class TwoWayStringVariableBinder @Inject constructor(
errorCollectors: ErrorCollectors,
expressionsRuntimeProvider: ExpressionsRuntimeProvider
@@ -21,6 +23,7 @@ internal class TwoWayStringVariableBinder @Inject constructor(
}
@DivScope
@Mockable
internal class TwoWayIntegerVariableBinder @Inject constructor(
errorCollectors: ErrorCollectors,
expressionsRuntimeProvider: ExpressionsRuntimeProvider
@@ -31,11 +34,13 @@ internal class TwoWayIntegerVariableBinder @Inject constructor(
override fun Long.toStringValue() = toString()
}
@Mockable
internal abstract class TwoWayVariableBinder<T>(
private val errorCollectors: ErrorCollectors,
private val expressionsRuntimeProvider: ExpressionsRuntimeProvider
) {
@Mockable
interface Callbacks<T> {
@MainThread
fun onVariableChanged(value: T?)
@@ -87,7 +87,7 @@ internal class DivSelectBinder @Inject constructor(
private fun DivSelectView.observeVariable(div: DivSelect, divView: Div2View, errorCollector: ErrorCollector) {
val resolver = divView.expressionResolver
variableBinder.bindVariable(
val subscription = variableBinder.bindVariable(
divView,
div.valueVariable,
callbacks = object : TwoWayStringVariableBinder.Callbacks {
@@ -116,6 +116,8 @@ internal class DivSelectBinder @Inject constructor(
this@observeVariable.valueUpdater = valueUpdater
}
})
addSubscription(subscription)
}
private fun DivSelectView.observeFontSize(div: DivSelect, resolver: ExpressionResolver) {
@@ -0,0 +1,112 @@
package com.yandex.div.core.view2.divs
import com.yandex.div.core.expression.variables.TwoWayStringVariableBinder
import com.yandex.div.core.expression.variables.TwoWayVariableBinder
import com.yandex.div.core.view2.DivTypefaceResolver
import com.yandex.div.core.view2.divs.widgets.DivSelectView
import com.yandex.div.core.view2.errors.ErrorCollectors
import com.yandex.div.internal.widget.SelectView
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import com.yandex.div2.DivSelect
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.robolectric.RobolectricTestRunner
@RunWith(RobolectricTestRunner::class)
class DivSelectBinderTest : DivBinderTest() {
private val divTypefaceResolver = mock<DivTypefaceResolver>()
private val variableBinder = mock<TwoWayStringVariableBinder>() {
on { bindVariable(any(), any(), any()) } doReturn mock()
}
private val errorCollectors = mock<ErrorCollectors> {
on { getOrCreate(anyOrNull(), anyOrNull()) } doReturn mock()
}
private val captor = argumentCaptor<TwoWayVariableBinder.Callbacks<String>>()
private val underTest = DivSelectBinder(
baseBinder = baseBinder,
typefaceResolver = divTypefaceResolver,
variableBinder = variableBinder,
errorCollectors = errorCollectors
)
private val div = UnitTestData(SELECT_DIR, "with_options.json").div as Div.Select
private val divSelect = div.value
private val view = (viewCreator.create(div, ExpressionResolver.EMPTY) as DivSelectView).apply {
layoutParams = defaultLayoutParams()
}
@Test
fun `bind value_variable`() {
underTest.bindView(view, divSelect, divView)
verify(variableBinder).bindVariable(any(), eq(divSelect.valueVariable), any())
verifyNoMoreInteractions(variableBinder)
}
@Test
fun `update text after variable changed`() {
underTest.bindView(view, divSelect, divView)
verify(variableBinder).bindVariable(any(), any(), captor.capture())
val (optionText, optionValue) = divSelect.options.evaluateLastOption()
view.assertTextApplied(optionText) {
val callbacks = captor.allValues.single()
callbacks.onVariableChanged(optionValue)
}
verifyNoMoreInteractions(variableBinder)
}
@Test
fun `update text and variable after option selected`() {
val viewStateChangeListener = mock<(String) -> Unit>()
underTest.bindView(view, divSelect, divView)
verify(variableBinder).bindVariable(any(), any(), captor.capture())
val (optionText, optionValue) = divSelect.options.evaluateLastOption()
view.assertTextApplied(optionText) {
val callbacks = captor.allValues.single()
callbacks.setViewStateChangeListener(viewStateChangeListener)
view.onItemSelectedListener!!(divSelect.options.size - 1)
}
verify(viewStateChangeListener).invoke(optionValue)
verifyNoMoreInteractions(viewStateChangeListener)
verifyNoMoreInteractions(variableBinder)
}
private fun List<DivSelect.Option>.evaluateLastOption(): Pair<String, String> {
val option = last()
val optionValue = option.value.evaluate(divView.expressionResolver)
val optionText = option.text?.evaluate(divView.expressionResolver) ?: optionValue
return optionText to optionValue
}
private inline fun SelectView.assertTextApplied(expectedText: String, body: () -> Unit) {
Assert.assertNotEquals(text, expectedText)
body()
Assert.assertEquals(text, expectedText)
}
companion object {
private const val SELECT_DIR = "div-select"
}
}
@@ -0,0 +1,127 @@
package com.yandex.div
import androidx.test.espresso.Espresso.pressBack
import androidx.test.rule.ActivityTestRule
import com.yandex.div.rule.uiTestRule
import com.yandex.div.steps.divSelect
import com.yandex.divkit.demo.DummyActivity
import com.yandex.test.util.assertNoPopupsAreDisplayed
import com.yandex.test.util.assertPopupDisplayed
import org.junit.Rule
import org.junit.Test
class DivSelectPopupVisibilityTest {
private val activityRule = ActivityTestRule(DummyActivity::class.java)
@get:Rule
val rule = uiTestRule { activityRule }
@Test
fun popupShown() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
clickOnSelect()
assertPopupDisplayed()
}
}
@Test
fun popupDismissedOnItemTap() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
clickOnSelect()
assertPopupDisplayed()
selectFirstOption()
assertNoPopupsAreDisplayed()
}
}
@Test
fun popupDismissedOnPressBack() {
// Press back should work the same as click outside of popup
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
clickOnSelect()
assertPopupDisplayed()
pressBack()
assertNoPopupsAreDisplayed()
}
}
@Test
fun popupDismissedOnVisibilityInvisible() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
openSelectWithDelayedAction("visibility_invisible")
assertNoPopupsAreDisplayed()
}
}
@Test
fun popupDismissedOnVisibilityGone() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
openSelectWithDelayedAction("visibility_gone")
assertNoPopupsAreDisplayed()
}
}
@Test
fun popupAlignedWithAnchor() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
val select = stealSelect()
clickOnSelect()
assertPopupDisplayed()
assert { popupAlignedWithAnchor(select) }
}
}
@Test
fun popupAlignedWithAnchorAfterWidthChange() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
val select = stealSelect()
openSelectWithDelayedAction("width_change")
assert { popupAlignedWithAnchor(select) }
}
}
@Test
fun popupAlignedWithAnchorAfterPositionChange() {
divSelect {
activityRule.buildContainer()
assertNoPopupsAreDisplayed()
val select = stealSelect()
openSelectWithDelayedAction("position_change")
assert { popupAlignedWithAnchor(select) }
}
}
}
@@ -0,0 +1,100 @@
package com.yandex.div.steps
import android.view.View
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import com.yandex.div.view.click
import com.yandex.div.view.tap
import com.yandex.divkit.demo.DummyActivity
import com.yandex.test.idling.SimpleIdlingResource
import com.yandex.test.idling.waitForIdlingResource
import com.yandex.test.util.Report.step
import com.yandex.test.util.StepsDsl
import com.yandex.test.util.assertPopupDisplayed
import org.hamcrest.Matchers
private const val TIMER_FINISHED_TEXT = "DONE"
internal fun divSelect(f: DivSelectSteps.() -> Unit) = f(DivSelectSteps())
@StepsDsl
class DivSelectSteps : DivTestAssetSteps() {
private val selectInteraction = onView(ViewMatchers.withHint("Select country"))
private val firstItemInteraction = onView(withText("United Kingdom"))
private val timerInteraction = onView(Matchers.anyOf(withText("IN PROGRESS")))
fun ActivityTestRule<DummyActivity>.buildContainer() {
testAsset = "ui_test_data/select/select_with_default.json"
buildContainer(MATCH_PARENT, MATCH_PARENT)
}
fun clickOnSelect(): Unit = step("Click on select") {
selectInteraction.click()
}
fun selectFirstOption(): Unit = step("Select first option") {
firstItemInteraction.tap()
}
fun openSelectWithDelayedAction(action: String) = step("Open select with delayed action") {
val timer = prepareTimerIdlingResource()
invokeDelayedAction(action)
clickOnSelect()
assertPopupDisplayed()
waitForIdlingResource(timer)
}
fun stealSelect(): View = step("Steal select view") {
selectInteraction.stealView()
}
private fun invokeDelayedAction(action: String): Unit =
step("Invoke action $action with delay") {
onView(withText(action)).click()
}
private fun prepareTimerIdlingResource(): IdlingResource =
step("Prepare timer idling resource") {
timerInteraction.stealView().asIdlingResource {
(this as TextView).text == TIMER_FINISHED_TEXT
}
}
private inline fun View.asIdlingResource(crossinline checkIdle: View.() -> Boolean) =
object : SimpleIdlingResource() {
override fun checkIdle(): Boolean = checkIdle(this@asIdlingResource)
}
private fun ViewInteraction.stealView(): View {
var stolenView: View? = null
check { view, _ -> stolenView = view }
return stolenView!!
}
fun assert(f: DivSelectAssertions.() -> Unit) = f(DivSelectAssertions())
}
@StepsDsl
class DivSelectAssertions {
private val popupInteraction = onView(ViewMatchers.withChild(withText("United Kingdom")))
fun popupAlignedWithAnchor(anchorView: View) {
popupInteraction.check { popup, _ ->
val (left, top) = IntArray(2).apply { popup.getLocationOnScreen(this) }
val (anchorLeft, anchorTop) = IntArray(2).apply { anchorView.getLocationOnScreen(this) }
assert(left == anchorLeft)
assert(top == anchorTop)
assert(popup.width == anchorView.width)
}
}
}
@@ -12,6 +12,12 @@ fun assertNoPopupsAreDisplayed() {
activeRoots.none { RootMatchers.isPlatformPopup().matches(it) })
}
fun assertPopupDisplayed() {
Assert.assertNotNull("Exactly one popup view must exist",
activeRoots.singleOrNull { RootMatchers.isPlatformPopup().matches(it) }
)
}
internal val activeRoots: List<Root>
get() = performOnMain {
Reflection.staticField("BASE")
@@ -0,0 +1,240 @@
{
"templates": {
"button": {
"type": "text",
"paddings": {
"bottom": 16,
"top": 16,
"left": 16,
"right": 16
}
}
},
"card": {
"log_id": "select",
"variables": [
{
"name": "value_variable",
"type": "string",
"value": ""
},
{
"name": "visibility",
"type": "string",
"value": "visible"
},
{
"name": "timer_finished",
"type": "string",
"value": "IN PROGRESS"
},
{
"name": "width",
"type": "integer",
"value": 200
},
{
"name": "margin",
"type": "integer",
"value": 16
}
],
"timers": [
{
"id": "visibility_invisible",
"duration": 1000,
"end_actions": [
{
"log_id": "end",
"url": "div-action://set_variable?name=visibility&value=invisible"
},
{
"log_id": "end",
"url": "div-action://set_variable?name=timer_finished&value=DONE"
}
]
},
{
"id": "visibility_gone",
"duration": 1000,
"end_actions": [
{
"log_id": "end",
"url": "div-action://set_variable?name=visibility&value=gone"
},
{
"log_id": "end",
"url": "div-action://set_variable?name=timer_finished&value=DONE"
}
]
},
{
"id": "width_change",
"duration": 1000,
"end_actions": [
{
"log_id": "end",
"url": "div-action://set_variable?name=width&value=150"
},
{
"log_id": "end",
"url": "div-action://set_variable?name=timer_finished&value=DONE"
}
]
},
{
"id": "position_change",
"duration": 1000,
"end_actions": [
{
"log_id": "end",
"url": "div-action://set_variable?name=margin&value=64"
},
{
"log_id": "end",
"url": "div-action://set_variable?name=timer_finished&value=DONE"
}
]
}
],
"states": [
{
"state_id": 0,
"div": {
"type": "container",
"width": {
"type": "match_parent"
},
"height": {
"type": "wrap_content"
},
"items": [
{
"type": "text",
"text": "@{timer_finished}"
},
{
"type": "select",
"width": {
"type": "fixed",
"value": "@{width}"
},
"height": {
"type": "wrap_content"
},
"margins": {
"left": "@{margin}",
"top": "@{margin}"
},
"paddings": {
"left": 16,
"top": 10,
"right": 16,
"bottom": 10
},
"alpha": 1,
"alignment_horizontal": "center",
"alignment_vertical": "center",
"background": [
{
"type": "solid",
"color": "#0e000000"
}
],
"border": {
"corner_radius": 8
},
"font_size": 16,
"font_weight": "medium",
"text_color": "#000000",
"value_variable": "value_variable",
"hint_text": "Select country",
"hint_color": "#888888",
"line_height": 22,
"visibility": "@{visibility}",
"options": [
{
"value": ""
},
{
"value": "ru",
"text": "Russia"
},
{
"value": "uk",
"text": "United Kingdom"
},
{
"value": "kz"
}
]
},
{
"type": "text",
"width": {
"type": "match_parent"
},
"height": {
"type": "wrap_content"
},
"paddings": {
"left": 18,
"right": 16,
"bottom": 16
},
"font_size": 16,
"text_color": "#000000",
"text": "Text: @{value_variable}"
},
{
"type": "text",
"text": "Timers",
"margins": {
"left": 16,
"top": 8
},
"font_size": 16
},
{
"type": "container",
"items": [
{
"type": "button",
"text": "visibility_invisible",
"action": {
"log_id": "visibility_invisible",
"url": "div-action://timer?id=visibility_invisible&action=start"
}
},
{
"type": "button",
"text": "visibility_gone",
"action": {
"log_id": "visibility_gone",
"url": "div-action://timer?id=visibility_gone&action=start"
}
},
{
"type": "button",
"text": "width_change",
"action": {
"log_id": "width_change",
"url": "div-action://timer?id=width_change&action=start"
}
},
{
"type": "button",
"text": "position_change",
"action": {
"log_id": "position_change",
"url": "div-action://timer?id=position_change&action=start"
}
}
]
}
]
}
}
]
}
}
@@ -0,0 +1,53 @@
{
"type": "select",
"width": {
"type": "match_parent"
},
"height": {
"type": "wrap_content"
},
"margins": {
"left": 16,
"top": 20,
"right": 16,
"bottom": 16
},
"paddings": {
"left": 16,
"top": 10,
"right": 16,
"bottom": 10
},
"alpha": 1.0,
"alignment_horizontal": "center",
"alignment_vertical": "center",
"background": [
{
"type": "solid",
"color": "#0e000000"
}
],
"border": {
"corner_radius": 8
},
"font_size": 16,
"font_weight": "medium",
"text_color": "#000000",
"value_variable": "value_variable",
"hint_text": "Select country",
"hint_color": "#888888",
"line_height": 22,
"options": [
{
"value": "ru",
"text": "Russia"
},
{
"value": "uk",
"text": "United Kingdom"
},
{
"value": "kz"
}
]
}