Added DivViewComponent

commit_hash:145be58fb45e34d115aeae19a4e8ba9f4ce4b96a
This commit is contained in:
pkurchatov
2026-04-14 17:35:47 +03:00
parent 1bb63cf567
commit db930d8f01
24 changed files with 421 additions and 229 deletions
@@ -3,7 +3,6 @@ package com.yandex.div.compose
import android.content.Context
import android.content.ContextWrapper
import androidx.annotation.VisibleForTesting
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.compose.context.DivViewContext
import com.yandex.div.compose.dagger.DivContextComponent
import com.yandex.div.compose.dagger.`Yatagan$DivContextComponent`
@@ -11,14 +10,7 @@ import com.yandex.div.compose.internal.DivDebugConfiguration
import com.yandex.div.compose.internal.DivDebugFeatures
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.internal.expressions.FunctionProviderDecorator
import com.yandex.div.internal.expressions.toLocalFunctions
import com.yandex.div2.DivBase
import com.yandex.div2.DivData
import com.yandex.div2.DivTrigger
import com.yandex.div2.DivVariable
/**
* An implementation of [android.content.Context] that must be used for composing [DivView]s.
@@ -70,68 +62,13 @@ class DivContext private constructor(
return it
}
val baseFunctionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider)
val functions = data.functions.orEmpty().toLocalFunctions()
return DivViewContext(
rootLocalContext = createLocalContext(
variableController = DivVariableController(component.variableController),
functionProvider = baseFunctionProvider + functions,
triggers = data.variableTriggers.orEmpty(),
variables = data.variables.orEmpty()
)
data = data,
component = component.viewComponent().build()
).also {
component.viewContextStorage.put(data, it)
}
}
internal fun getLocalContext(
data: DivBase,
viewContext: DivViewContext,
parentContext: DivLocalContext
): DivLocalContext {
viewContext.localContextStorage.get(data)?.let {
return it
}
val functions = data.functions.orEmpty().toLocalFunctions()
val variables = data.variables.orEmpty()
return createLocalContext(
variableController = if (variables.isEmpty()) {
parentContext.variableController
} else {
DivVariableController(parentContext.variableController)
},
functionProvider = parentContext.functionProvider + functions,
triggers = data.variableTriggers.orEmpty(),
variables = variables
).also {
viewContext.localContextStorage.put(data, it)
}
}
private fun createLocalContext(
variableController: DivVariableController,
functionProvider: FunctionProviderDecorator,
triggers: List<DivTrigger>,
variables: List<DivVariable>,
): DivLocalContext {
val localComponent = component.localComponent()
.functionProvider(functionProvider)
.variableController(variableController)
.build()
variables.forEach { variableData ->
localComponent.variableAdapter.convert(variableData)?.let {
variableController.declare(it)
}
}
triggers.forEach {
localComponent.triggerStorage.add(it)
}
return localComponent.context
}
}
private fun createComponent(
@@ -139,9 +76,9 @@ private fun createComponent(
configuration: DivComposeConfiguration,
debugConfiguration: DivDebugConfiguration
): DivContextComponent {
return `Yatagan$DivContextComponent`.builder()
.baseContext(baseContext)
.configuration(configuration)
.debugConfiguration(debugConfiguration)
.build()
return `Yatagan$DivContextComponent`.builder().build(
baseContext = baseContext,
configuration = configuration,
debugConfiguration = debugConfiguration
)
}
@@ -3,8 +3,8 @@ package com.yandex.div.compose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.context.LocalDivViewContext
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.triggers.observe
import com.yandex.div.compose.utils.divContext
import com.yandex.div.compose.utils.reporter
@@ -46,10 +46,11 @@ fun DivView(
}
val viewContext = divContext.getViewContext(data)
viewContext.rootLocalContext.triggerStorage.observe()
val localComponent = viewContext.rootLocalComponent
localComponent.triggerStorage.observe()
CompositionLocalProvider(
LocalDivViewContext provides viewContext,
LocalDivContext provides viewContext.rootLocalContext
LocalComponent provides localComponent
) {
DivBlockView(
data = div,
@@ -1,7 +1,7 @@
package com.yandex.div.compose.actions
import com.yandex.div.compose.dagger.DivLocalScope
import com.yandex.div.core.annotations.ExperimentalApi
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.json.expressions.ExpressionResolver
import javax.inject.Inject
@@ -9,7 +9,7 @@ import javax.inject.Inject
* Context passed to action handlers. Provides access to DivKit components
* scoped to the element that triggered the action.
*/
@Mockable
@DivLocalScope
@ExperimentalApi
class DivActionHandlingContext @Inject internal constructor(
val expressionResolver: ExpressionResolver
@@ -1,6 +1,6 @@
package com.yandex.div.compose.actions
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.compose.dagger.DivViewScope
import com.yandex.div2.DivDisappearAction
import com.yandex.div2.DivSightAction
import com.yandex.div2.DivVisibilityAction
@@ -10,7 +10,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@DivContextScope
@DivViewScope
internal class VisibilityActionTracker @Inject constructor(
private val actionHandler: DivActionHandler,
private val coroutineScope: CoroutineScope
@@ -0,0 +1,19 @@
package com.yandex.div.compose.context
import com.yandex.div.compose.dagger.DivLocalComponent
import com.yandex.div.compose.dagger.DivViewScope
import com.yandex.div2.DivBase
import javax.inject.Inject
@DivViewScope
internal class DivLocalComponentStorage @Inject constructor() {
private val items = mutableMapOf<DivBase, DivLocalComponent>()
fun get(div: DivBase): DivLocalComponent? {
return items[div]
}
fun put(div: DivBase, context: DivLocalComponent) {
items[div] = context
}
}
@@ -1,47 +0,0 @@
package com.yandex.div.compose.context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import com.yandex.div.compose.DivException
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.dagger.DivLocalScope
import com.yandex.div.compose.triggers.DivTriggerStorage
import com.yandex.div.compose.triggers.observe
import com.yandex.div.compose.utils.divContext
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.internal.expressions.FunctionProviderDecorator
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivBase
import javax.inject.Inject
@DivLocalScope
internal class DivLocalContext @Inject constructor(
val actionHandlingContext: DivActionHandlingContext,
val expressionResolver: ExpressionResolver,
val functionProvider: FunctionProviderDecorator,
val triggerStorage: DivTriggerStorage,
val variableController: DivVariableController
)
internal val LocalDivContext = compositionLocalOf<DivLocalContext> {
throw DivException("DivLocalContext not provided")
}
@Composable
internal fun WithLocalDivContext(data: DivBase, content: @Composable () -> Unit) {
val functions = data.functions.orEmpty()
val variables = data.variables.orEmpty()
val triggers = data.variableTriggers.orEmpty()
if (functions.isEmpty() && variables.isEmpty() && triggers.isEmpty()) {
return content()
}
val localContext = divContext.getLocalContext(
data,
viewContext = LocalDivViewContext.current,
parentContext = LocalDivContext.current
)
localContext.triggerStorage.observe()
CompositionLocalProvider(LocalDivContext provides localContext, content)
}
@@ -1,15 +0,0 @@
package com.yandex.div.compose.context
import com.yandex.div2.DivBase
internal class DivLocalContextStorage() {
private val items = mutableMapOf<DivBase, DivLocalContext>()
fun get(div: DivBase): DivLocalContext? {
return items[div]
}
fun put(div: DivBase, context: DivLocalContext) {
items[div] = context
}
}
@@ -2,14 +2,88 @@ package com.yandex.div.compose.context
import androidx.compose.runtime.compositionLocalOf
import com.yandex.div.compose.DivException
import com.yandex.div.compose.actions.VisibilityActionTracker
import com.yandex.div.compose.dagger.DivLocalComponent
import com.yandex.div.compose.dagger.DivViewComponent
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.internal.expressions.FunctionProviderDecorator
import com.yandex.div.internal.expressions.toLocalFunctions
import com.yandex.div2.DivBase
import com.yandex.div2.DivData
import com.yandex.div2.DivTrigger
import com.yandex.div2.DivVariable
import kotlin.collections.forEach
import kotlin.collections.orEmpty
internal class DivViewContext(
val rootLocalContext: DivLocalContext
data: DivData,
private val component: DivViewComponent
) {
/**
* Stores [DivLocalContext]s for view elements.
*/
val localContextStorage = DivLocalContextStorage()
val rootLocalComponent: DivLocalComponent
val visibilityActionTracker: VisibilityActionTracker
get() = component.visibilityActionTracker
init {
val baseFunctionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider)
val functions = data.functions.orEmpty().toLocalFunctions()
rootLocalComponent = createLocalComponent(
variableController = DivVariableController(component.variableController),
functionProvider = baseFunctionProvider + functions,
triggers = data.variableTriggers.orEmpty(),
variables = data.variables.orEmpty()
)
}
fun getLocalComponent(
data: DivBase,
parentComponent: DivLocalComponent
): DivLocalComponent {
component.localComponentStorage.get(data)?.let {
return it
}
val functions = data.functions.orEmpty().toLocalFunctions()
val variables = data.variables.orEmpty()
return createLocalComponent(
variableController = if (variables.isEmpty()) {
parentComponent.variableController
} else {
DivVariableController(parentComponent.variableController)
},
functionProvider = parentComponent.functionProvider + functions,
triggers = data.variableTriggers.orEmpty(),
variables = variables
).also {
component.localComponentStorage.put(data, it)
}
}
private fun createLocalComponent(
variableController: DivVariableController,
functionProvider: FunctionProviderDecorator,
triggers: List<DivTrigger>,
variables: List<DivVariable>,
): DivLocalComponent {
val localComponent = component.localComponent().build(
functionProvider = functionProvider,
variableController = variableController
)
variables.forEach { variableData ->
localComponent.variableAdapter.convert(variableData)?.let {
variableController.declare(it)
}
}
triggers.forEach {
localComponent.triggerStorage.add(it)
}
return localComponent
}
}
internal val LocalDivViewContext = compositionLocalOf<DivViewContext> {
@@ -6,7 +6,6 @@ import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivFontFamilyProvider
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.actions.VisibilityActionTracker
import com.yandex.div.compose.context.DivViewContextStorage
import com.yandex.div.compose.internal.DivDebugConfiguration
import com.yandex.div.compose.internal.DivDebugFeatures
@@ -31,25 +30,18 @@ internal interface DivContextComponent {
val imageLoader: ImageLoader
val reporter: DivReporter
val viewContextStorage: DivViewContextStorage
val visibilityActionTracker: VisibilityActionTracker
@get:Named(Names.HOST_VARIABLES)
val variableController: DivVariableController
fun localComponent(): DivLocalComponent.Builder
fun viewComponent(): DivViewComponent.Builder
@Component.Builder
interface Builder {
@BindsInstance
fun baseContext(baseContext: Context): Builder
@BindsInstance
fun configuration(configuration: DivComposeConfiguration): Builder
@BindsInstance
fun debugConfiguration(configuration: DivDebugConfiguration): Builder
fun build(): DivContextComponent
fun build(
@BindsInstance baseContext: Context,
@BindsInstance configuration: DivComposeConfiguration,
@BindsInstance debugConfiguration: DivDebugConfiguration
): DivContextComponent
}
}
@@ -1,12 +1,21 @@
package com.yandex.div.compose.dagger
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import com.yandex.div.compose.DivException
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.context.LocalDivViewContext
import com.yandex.div.compose.triggers.DivTriggerStorage
import com.yandex.div.compose.triggers.observe
import com.yandex.div.compose.variables.DivVariableAdapter
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.internal.expressions.FunctionProviderDecorator
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivBase
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
import kotlin.collections.orEmpty
@DivLocalScope
@Component(
@@ -17,19 +26,39 @@ import com.yandex.yatagan.Component
)
internal interface DivLocalComponent {
val context: DivLocalContext
val actionHandlingContext: DivActionHandlingContext
val expressionResolver: ExpressionResolver
val functionProvider: FunctionProviderDecorator
val triggerStorage: DivTriggerStorage
val variableAdapter: DivVariableAdapter
val variableController: DivVariableController
@Component.Builder
interface Builder {
@BindsInstance
fun functionProvider(functionProvider: FunctionProviderDecorator): Builder
@BindsInstance
fun variableController(variableController: DivVariableController): Builder
fun build(): DivLocalComponent
fun build(
@BindsInstance functionProvider: FunctionProviderDecorator,
@BindsInstance variableController: DivVariableController
): DivLocalComponent
}
}
internal val LocalComponent = compositionLocalOf<DivLocalComponent> {
throw DivException("DivLocalComponent not provided")
}
@Composable
internal fun WithLocalComponent(data: DivBase, content: @Composable () -> Unit) {
val functions = data.functions.orEmpty()
val variables = data.variables.orEmpty()
val triggers = data.variableTriggers.orEmpty()
if (functions.isEmpty() && variables.isEmpty() && triggers.isEmpty()) {
return content()
}
val localComponent = LocalDivViewContext.current.getLocalComponent(
data,
parentComponent = LocalComponent.current
)
localComponent.triggerStorage.observe()
CompositionLocalProvider(LocalComponent provides localComponent, content)
}
@@ -0,0 +1,25 @@
package com.yandex.div.compose.dagger
import com.yandex.div.compose.actions.VisibilityActionTracker
import com.yandex.div.compose.context.DivLocalComponentStorage
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.yatagan.Component
import javax.inject.Named
@DivViewScope
@Component(isRoot = false)
internal interface DivViewComponent {
val localComponentStorage: DivLocalComponentStorage
val visibilityActionTracker: VisibilityActionTracker
@get:Named(Names.HOST_VARIABLES)
val variableController: DivVariableController
fun localComponent(): DivLocalComponent.Builder
@Component.Builder
interface Builder {
fun build(): DivViewComponent
}
}
@@ -0,0 +1,7 @@
package com.yandex.div.compose.dagger
import javax.inject.Scope
@Scope
@Retention(AnnotationRetention.RUNTIME)
internal annotation class DivViewScope
@@ -26,7 +26,7 @@ class DivDebugFeatures @Inject internal constructor(
* Returns [ExpressionResolver] associated with the given [DivData].
*/
fun getExpressionResolver(data: DivData): ExpressionResolver? {
return viewContextStorage.get(data)?.rootLocalContext?.expressionResolver
return viewContextStorage.get(data)?.rootLocalComponent?.expressionResolver
}
/**
@@ -36,7 +36,7 @@ class DivDebugFeatures @Inject internal constructor(
fun performAction(data: DivData, action: DivAction) {
viewContextStorage.get(data)?.let {
actionHandler.handle(
context = it.rootLocalContext.actionHandlingContext,
context = it.rootLocalComponent.actionHandlingContext,
action = action,
source = DivActionSource.EXTERNAL
)
@@ -6,13 +6,17 @@ import coil3.ImageLoader
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.DivFontFamilyProvider
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.json.expressions.ExpressionResolver
internal val divContext: DivContext
@Composable
get() = LocalContext.current as DivContext
internal val fontFamilyProvider: DivFontFamilyProvider
@Composable
get() = divContext.component.fontFamilyProvider
internal val imageLoader: ImageLoader
@Composable
get() = divContext.component.imageLoader
@@ -21,10 +25,6 @@ internal val reporter: DivReporter
@Composable
get() = divContext.component.reporter
internal val fontFamilyProvider: DivFontFamilyProvider
@Composable
get() = divContext.component.fontFamilyProvider
internal val expressionResolver: ExpressionResolver
@Composable
get() = LocalDivContext.current.expressionResolver
get() = LocalComponent.current.expressionResolver
@@ -1,6 +1,6 @@
package com.yandex.div.compose.variables
import com.yandex.div.core.annotations.Mockable
import com.yandex.div.compose.dagger.DivLocalScope
import com.yandex.div.data.Variable
import com.yandex.div.internal.data.PropertyVariableExecutor
import com.yandex.div.internal.variables.toVariable
@@ -9,7 +9,7 @@ import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivVariable
import javax.inject.Inject
@Mockable
@DivLocalScope
internal class DivVariableAdapter @Inject constructor(
private val expressionResolver: ExpressionResolver,
private val parsingErrorLogger: ParsingErrorLogger
@@ -2,7 +2,7 @@ package com.yandex.div.compose.views
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.yandex.div.compose.context.WithLocalDivContext
import com.yandex.div.compose.dagger.WithLocalComponent
import com.yandex.div.compose.views.container.DivContainerView
import com.yandex.div.compose.views.gallery.DivGalleryView
import com.yandex.div.compose.views.image.DivImageView
@@ -21,9 +21,9 @@ internal fun DivBlockView(
modifier: Modifier = Modifier,
applyMargins: Boolean = true,
) {
WithLocalDivContext(data.value()) {
WithLocalComponent(data.value()) {
if (data.value().visibility.observedValue() == DivVisibility.GONE) {
return@WithLocalDivContext
return@WithLocalComponent
}
val modifier = modifier.apply(data, applyMargins = applyMargins)
@@ -12,11 +12,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import com.yandex.div.compose.actions.DivActionSource
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.utils.observedFloatValue
import com.yandex.div.compose.utils.observedIntValue
import com.yandex.div.compose.utils.observedValue
import com.yandex.div.compose.utils.reporter
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.utils.divContext
import com.yandex.div2.Div
import com.yandex.div2.DivAction
@@ -32,7 +32,7 @@ internal fun Modifier.actions(data: Div): Modifier {
}
val actionHandler = divContext.component.actionHandler
val actionHandlingContext = LocalDivContext.current.actionHandlingContext
val actionHandlingContext = LocalComponent.current.actionHandlingContext
val onClick: () -> Unit = {
actionHandler.handle(actionHandlingContext, actions, source = DivActionSource.TAP)
}
@@ -3,8 +3,8 @@ package com.yandex.div.compose.views.modifiers
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onVisibilityChanged
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.utils.divContext
import com.yandex.div.compose.context.LocalDivViewContext
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.utils.observedIntValue
import com.yandex.div.compose.utils.observedValue
import com.yandex.div2.DivBase
@@ -24,8 +24,8 @@ internal fun Modifier.visibilityActions(data: DivBase): Modifier {
@Composable
private fun Modifier.visibilityActions(actions: List<DivVisibilityAction>): Modifier {
val visibilityActionTracker = divContext.component.visibilityActionTracker
val actionHandlingContext = LocalDivContext.current.actionHandlingContext
val visibilityActionTracker = LocalDivViewContext.current.visibilityActionTracker
val actionHandlingContext = LocalComponent.current.actionHandlingContext
var modifier = this
actions
.filter { shouldRegisterVisibilityCallback(it) }
@@ -46,8 +46,8 @@ private fun Modifier.visibilityActions(actions: List<DivVisibilityAction>): Modi
@Composable
private fun Modifier.disappearActions(actions: List<DivDisappearAction>): Modifier {
val visibilityActionTracker = divContext.component.visibilityActionTracker
val actionHandlingContext = LocalDivContext.current.actionHandlingContext
val visibilityActionTracker = LocalDivViewContext.current.visibilityActionTracker
val actionHandlingContext = LocalComponent.current.actionHandlingContext
var modifier = this
actions
.filter { shouldRegisterVisibilityCallback(it) }
@@ -67,7 +67,7 @@ private fun Modifier.disappearActions(actions: List<DivDisappearAction>): Modifi
@Composable
private fun shouldRegisterVisibilityCallback(action: DivSightAction): Boolean {
return !divContext.component.visibilityActionTracker.isLimitReached(
return !LocalDivViewContext.current.visibilityActionTracker.isLimitReached(
action = action,
limit = action.logLimit.observedIntValue()
)
@@ -2,7 +2,7 @@ package com.yandex.div.compose.views.state
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.compose.utils.expressionResolver
import com.yandex.div.compose.utils.observedVariableValue
import com.yandex.div.compose.utils.reporter
@@ -16,7 +16,7 @@ internal fun DivState.observeActiveState(): DivState.State? {
}
val activeStateId = stateVariable?.let {
LocalDivContext.current.variableController.observedVariableValue(it)
LocalComponent.current.variableController.observedVariableValue(it)
}
val resolver = expressionResolver
@@ -0,0 +1,174 @@
package com.yandex.div.compose
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.compose.internal.DivDebugConfiguration
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.Variable
import com.yandex.div.test.data.data
import com.yandex.div.test.data.expression
import com.yandex.div.test.data.intExpression
import com.yandex.div.test.data.setVariableAction
import com.yandex.div.test.data.text
import com.yandex.div.test.data.typedValue
import com.yandex.div.test.data.visibilityAction
import com.yandex.div.test.data.visibilityExpression
import com.yandex.div2.DivActionTyped
import com.yandex.div2.DivData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.math.min
@RunWith(AndroidJUnit4::class)
class DivViewWithVisibilityActionsRecompositionTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>().apply {
mainClock.autoAdvance = false
}
private val activity: ComponentActivity
get() = rule.activity
private val counter = Variable.IntegerVariable("counter", 0)
private val visibility = Variable.StringVariable("visibility", "visible")
private val variableController = DivVariableController().apply {
declare(counter, visibility)
}
private val testScope = TestScope()
private lateinit var divContext: DivContext
@Before
fun setUp() {
divContext = DivContext(
baseContext = activity,
configuration = DivComposeConfiguration(
reporter = TestReporter(),
variableController = variableController
),
debugConfiguration = DivDebugConfiguration(
coroutineScope = testScope
)
)
}
@Test
fun `visibility action limit does not reset when ComposeView is recreated with the same context`() {
val data = data(
content = text(
id = "counter",
text = expression("counter = @{counter}"),
visibility = visibilityExpression("@{visibility}"),
visibilityActions = listOf(
visibilityAction(delayMs = 500, limit = 2, typed = incrementCounterAction())
)
)
)
setContent(data)
rule.onNodeWithTag("counter").assertTextEquals("counter = 0")
advanceTimeBy(500)
rule.onNodeWithTag("counter").assertTextEquals("counter = 1")
activity.setContentView(View(activity))
setContent(data)
rule.onNodeWithTag("counter").assertTextEquals("counter = 1")
advanceTimeBy(500)
repeat(5) {
rule.onNodeWithTag("counter").assertTextEquals("counter = 2")
hideCounter()
showCounter()
advanceTimeBy(500)
}
}
@Test
fun `visibility action limit resets when the view context was cleared`() {
val data = data(
content = text(
id = "counter",
text = expression("counter = @{counter}"),
visibility = visibilityExpression("@{visibility}"),
visibilityActions = listOf(
visibilityAction(delayMs = 500, limit = 3, typed = incrementCounterAction())
)
)
)
setContent(data)
rule.onNodeWithTag("counter").assertTextEquals("counter = 0")
advanceTimeBy(500)
rule.onNodeWithTag("counter").assertTextEquals("counter = 1")
activity.setContentView(View(activity))
divContext.clearViewContext(data)
counter.set(0)
setContent(data)
repeat(5) {
rule.onNodeWithTag("counter").assertTextEquals("counter = ${min(it, 3)}")
hideCounter()
showCounter()
advanceTimeBy(500)
}
}
private fun setContent(data: DivData) {
activity.setContentView(
ComposeView(divContext).apply {
setContent {
DivView(data)
}
}
)
}
private fun showCounter() {
withAutoAdvance {
visibility.set("visible")
rule.onNodeWithTag("counter").assertIsDisplayed()
}
}
private fun hideCounter() {
withAutoAdvance {
visibility.set("gone")
rule.onNodeWithTag("counter").assertDoesNotExist()
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun advanceTimeBy(duration: Long) {
testScope.testScheduler.advanceTimeBy(duration)
testScope.testScheduler.runCurrent()
rule.mainClock.advanceTimeBy(duration)
rule.mainClock.advanceTimeByFrame()
}
private fun withAutoAdvance(block: () -> Unit) {
rule.mainClock.autoAdvance = true
block()
rule.mainClock.autoAdvance = false
}
}
private fun incrementCounterAction(): DivActionTyped {
return setVariableAction("counter", typedValue(intExpression("@{counter + 1}")))
}
@@ -11,9 +11,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.TestReporter
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.createExpressionResolver
import com.yandex.div.compose.dagger.DivLocalComponent
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.Variable
import org.junit.Assert.assertEquals
@@ -21,6 +21,7 @@ import org.junit.Assert.assertNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
@RunWith(AndroidJUnit4::class)
@@ -29,19 +30,16 @@ class DivVariableControllerTest {
@get:Rule
val composeRule = createComposeRule()
private val variableController = DivVariableController()
private val reporter = TestReporter()
private val variableController = DivVariableController()
private val localContext = DivLocalContext(
actionHandlingContext = mock(),
expressionResolver = createExpressionResolver(
private val localComponent = mock<DivLocalComponent> {
on { expressionResolver } doReturn createExpressionResolver(
reporter = reporter,
variableController = variableController
),
functionProvider = mock(),
triggerStorage = mock(),
variableController = variableController
)
)
on { variableController } doReturn variableController
}
@Test
fun `returns current value of string variable`() {
@@ -156,7 +154,7 @@ class DivVariableControllerTest {
)
CompositionLocalProvider(
LocalContext provides divContext,
LocalDivContext provides localContext,
LocalComponent provides localComponent,
) {
content()
}
@@ -10,8 +10,8 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.compose.createExpressionResolver
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.dagger.DivLocalComponent
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.Variable
import com.yandex.div.json.expressions.Expression
@@ -20,6 +20,7 @@ import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
@RunWith(AndroidJUnit4::class)
@@ -30,15 +31,12 @@ class ExpressionUtilsTest {
private val variableController = DivVariableController()
private val localContext = DivLocalContext(
actionHandlingContext = mock(),
expressionResolver = createExpressionResolver(
private val localComponent = mock<DivLocalComponent> {
on { expressionResolver } doReturn createExpressionResolver(
variableController = variableController
),
functionProvider = mock(),
triggerStorage = mock(),
variableController = variableController
)
)
on { variableController } doReturn variableController
}
@Test
fun `observed value changes when variable changes`() {
@@ -213,7 +211,7 @@ class ExpressionUtilsTest {
private fun setContent(content: @Composable () -> Unit) {
composeRule.setContent {
CompositionLocalProvider(LocalDivContext provides localContext) {
CompositionLocalProvider(LocalComponent provides localComponent) {
content()
}
}
@@ -11,9 +11,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.TestReporter
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.compose.createExpressionResolver
import com.yandex.div.compose.dagger.DivLocalComponent
import com.yandex.div.compose.dagger.LocalComponent
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.DivModelInternalApi
import com.yandex.div.data.Variable
@@ -26,6 +26,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
@OptIn(DivModelInternalApi::class)
@@ -35,19 +36,16 @@ class ObserveActiveStateTest {
@get:Rule
val composeRule = createComposeRule()
private val variableController = DivVariableController()
private val reporter = TestReporter()
private val variableController = DivVariableController()
private val localContext = DivLocalContext(
actionHandlingContext = mock(),
expressionResolver = createExpressionResolver(
private val localComponent = mock<DivLocalComponent> {
on { expressionResolver } doReturn createExpressionResolver(
reporter = reporter,
variableController = variableController
),
functionProvider = mock(),
triggerStorage = mock(),
variableController = variableController
)
)
on { variableController } doReturn variableController
}
@Test
fun `resolves state by stateIdVariable`() {
@@ -200,7 +198,7 @@ class ObserveActiveStateTest {
)
CompositionLocalProvider(
LocalContext provides divContext,
LocalDivContext provides localContext,
LocalComponent provides localComponent,
) {
content()
}