Added DivLocalContext storage

commit_hash:08dcc6c443daa3bb8d952b0637bf492706801c2c
This commit is contained in:
pkurchatov
2026-03-30 23:33:07 +03:00
parent c00e339693
commit 8537dfbad0
23 changed files with 603 additions and 123 deletions
+5 -1
View File
@@ -613,6 +613,10 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivActionHandlingContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/DivCustomActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/actions/SetVariableActionHandler.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivLocalContextStorage.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/context/DivViewContextStorage.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextComponent.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextModule.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextModule.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextScope.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/DivContextScope.kt",
@@ -634,7 +638,6 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/UnitConversions.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/UnitConversions.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/variables/DivVariableAdapter.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivBlockView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivLocalContext.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSeparatorView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivSeparatorView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/DivTextView.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/container/ContainerAlignment.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/container/ContainerAlignment.kt",
@@ -659,6 +662,7 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BorderModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/BorderModifiers.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/Modifiers.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/views/modifiers/SizeModifiers.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewRecompositionTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithFunctionsTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/DivViewWithTriggersTest.kt",
@@ -3,13 +3,18 @@ package com.yandex.div.compose
import android.content.ContextWrapper
import androidx.annotation.MainThread
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.internal.DivDebugFeatures
import com.yandex.div.compose.views.DivLocalContext
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.annotations.PublicApi
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 javax.inject.Inject
@@ -32,7 +37,58 @@ class DivContext @Inject @MainThread internal constructor(
val debugFeatures: DivDebugFeatures
get() = component.debugFeatures
internal fun createLocalContext(
/**
* Removes [DivView] context associated with the given [com.yandex.div2.DivData].
*/
fun clearViewContext(data: DivData) {
component.viewContextStorage.remove(data)
}
internal fun getViewContext(data: DivData): DivViewContext {
component.viewContextStorage.get(data)?.let {
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()
)
).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>,
@@ -1,10 +1,14 @@
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.triggers.observe
import com.yandex.div.compose.utils.divContext
import com.yandex.div.compose.utils.reporter
import com.yandex.div.compose.views.DivBlockView
import com.yandex.div.compose.views.WithLocalDivContext
import com.yandex.div2.DivData
/**
@@ -39,7 +43,12 @@ fun DivView(
return
}
WithLocalDivContext(data) {
val viewContext = divContext.getViewContext(data)
viewContext.rootLocalContext.triggerStorage.observe()
CompositionLocalProvider(
LocalDivViewContext provides viewContext,
LocalDivContext provides viewContext.rootLocalContext
) {
DivBlockView(
data = div,
modifier = modifier
@@ -0,0 +1,47 @@
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)
}
@@ -0,0 +1,15 @@
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
}
}
@@ -0,0 +1,17 @@
package com.yandex.div.compose.context
import androidx.compose.runtime.compositionLocalOf
import com.yandex.div.compose.DivException
internal class DivViewContext(
val rootLocalContext: DivLocalContext
) {
/**
* Stores [DivLocalContext]s for view elements.
*/
val localContextStorage = DivLocalContextStorage()
}
internal val LocalDivViewContext = compositionLocalOf<DivViewContext> {
throw DivException("DivViewContext not provided")
}
@@ -0,0 +1,22 @@
package com.yandex.div.compose.context
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div2.DivData
import javax.inject.Inject
@DivContextScope
internal class DivViewContextStorage @Inject constructor() {
private val items = mutableMapOf<DivData, DivViewContext>()
fun get(data: DivData): DivViewContext? {
return items[data]
}
fun put(data: DivData, context: DivViewContext) {
items[data] = context
}
fun remove(data: DivData) {
items.remove(data)
}
}
@@ -5,8 +5,9 @@ import coil3.ImageLoader
import com.yandex.div.compose.DivComposeConfiguration
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.compose.context.DivViewContextStorage
import com.yandex.div.compose.internal.DivDebugFeatures
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
import javax.inject.Named
@@ -25,6 +26,7 @@ internal interface DivContextComponent {
val debugFeatures: DivDebugFeatures
val imageLoader: ImageLoader
val reporter: DivReporter
val viewContextStorage: DivViewContextStorage
@get:Named(Names.HOST_VARIABLES)
val variableController: DivVariableController
@@ -2,7 +2,7 @@ package com.yandex.div.compose.dagger
import com.yandex.div.compose.triggers.DivTriggerStorage
import com.yandex.div.compose.variables.DivVariableAdapter
import com.yandex.div.compose.views.DivLocalContext
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.yatagan.BindsInstance
@@ -1,26 +1,41 @@
package com.yandex.div.compose.internal
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.context.DivViewContextStorage
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.compose.views.DivLocalContext
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivAction
import com.yandex.div2.DivData
import javax.inject.Inject
/**
* Provides access to the features not intended to be used in production environment.
*/
@DivContextScope
@InternalApi
class DivDebugFeatures @Inject internal constructor(
private val actionHandler: DivActionHandler
private val actionHandler: DivActionHandler,
private val viewContextStorage: DivViewContextStorage
) {
internal var lastViewLocalContext: DivLocalContext? = null
val expressionResolver: ExpressionResolver?
get() = lastViewLocalContext?.expressionResolver
/**
* Returns [ExpressionResolver] associated with the given [DivData].
*/
fun getExpressionResolver(data: DivData): ExpressionResolver? {
return viewContextStorage.get(data)?.rootLocalContext?.expressionResolver
}
fun performAction(action: DivAction) {
lastViewLocalContext?.let {
actionHandler.handle(context = it.actionHandlingContext, action = action)
/**
* Performs action in the context of [com.yandex.div.compose.DivView] associated with
* the given [DivData].
*/
fun performAction(data: DivData, action: DivAction) {
viewContextStorage.get(data)?.let {
actionHandler.handle(
context = it.rootLocalContext.actionHandlingContext,
action = action
)
}
}
}
@@ -1,5 +1,7 @@
package com.yandex.div.compose.triggers
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.dagger.DivLocalScope
@@ -19,18 +21,13 @@ internal class DivTriggerStorage @Inject constructor(
private class Item(
private val trigger: DivTrigger,
expressionResolver: ExpressionResolver,
private val expressionResolver: ExpressionResolver,
private val handleActions: (actions: List<DivAction>) -> Unit
) {
private val mode = trigger.mode.evaluate(expressionResolver)
private val conditionSubscription: Disposable
private var lastCondition: Boolean = false
init {
conditionSubscription = trigger.condition
.observeAndGet(expressionResolver, this::onConditionChanged)
}
private var conditionSubscription: Disposable? = null
private fun onConditionChanged(condition: Boolean) {
when (mode) {
@@ -47,12 +44,42 @@ internal class DivTriggerStorage @Inject constructor(
}
}
}
fun startObserving() {
conditionSubscription = trigger.condition
.observeAndGet(expressionResolver, this::onConditionChanged)
}
fun stopObserving() {
conditionSubscription?.close()
conditionSubscription = null
}
}
private val items = mutableListOf<Item>()
private var isActive = true
fun add(trigger: DivTrigger) {
items.add(Item(trigger, expressionResolver, this::handle))
val item = Item(trigger, expressionResolver, this::handle)
items.add(item)
if (isActive) {
item.startObserving()
}
}
fun startObserving() {
if (!isActive) {
items.forEach { it.startObserving() }
isActive = true
}
}
fun stopObserving() {
if (isActive) {
items.forEach { it.stopObserving() }
isActive = false
}
}
private fun handle(actions: List<DivAction>) {
@@ -61,3 +88,13 @@ internal class DivTriggerStorage @Inject constructor(
}
}
}
@Composable
internal fun DivTriggerStorage.observe() {
DisposableEffect(this) {
startObserving()
onDispose {
stopObserving()
}
}
}
@@ -6,7 +6,7 @@ import coil3.ImageLoader
import com.yandex.div.compose.DivContext
import com.yandex.div.compose.DivReporter
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.views.LocalDivContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.json.expressions.ExpressionResolver
internal val divContext: DivContext
@@ -2,6 +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.views.container.DivContainerView
import com.yandex.div.compose.views.gallery.DivGalleryView
import com.yandex.div.compose.views.image.DivImageView
@@ -1,75 +0,0 @@
package com.yandex.div.compose.views
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
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.utils.divContext
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.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivBase
import com.yandex.div2.DivData
import javax.inject.Inject
@DivLocalScope
internal class DivLocalContext @Inject constructor(
val actionHandlingContext: DivActionHandlingContext,
val expressionResolver: ExpressionResolver,
val functionProvider: FunctionProviderDecorator,
val variableController: DivVariableController
)
internal val LocalDivContext = compositionLocalOf<DivLocalContext> {
throw DivException("DivLocalContext not provided")
}
@Composable
internal fun WithLocalDivContext(data: DivData, content: @Composable () -> Unit) {
val divContext = divContext
val localContext = remember(data) {
val baseFunctionProvider = FunctionProviderDecorator(GeneratedBuiltinFunctionProvider)
val functions = data.functions.orEmpty().toLocalFunctions()
divContext.createLocalContext(
variableController = DivVariableController(divContext.component.variableController),
functionProvider = baseFunctionProvider + functions,
triggers = data.variableTriggers.orEmpty(),
variables = data.variables.orEmpty()
).also {
divContext.debugFeatures.lastViewLocalContext = it
}
}
CompositionLocalProvider(LocalDivContext provides localContext, content)
}
@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 divContext = divContext
val localContext = LocalDivContext.current
val newLocalContext = remember(data) {
val variableController = if (variables.isEmpty()) {
localContext.variableController
} else {
DivVariableController(localContext.variableController)
}
divContext.createLocalContext(
variableController = variableController,
functionProvider = localContext.functionProvider + functions.toLocalFunctions(),
triggers = triggers,
variables = variables
)
}
CompositionLocalProvider(LocalDivContext provides newLocalContext, content)
}
@@ -16,7 +16,7 @@ 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.views.LocalDivContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div2.Div
import com.yandex.div2.DivAction
import com.yandex.div2.DivAnimation
@@ -0,0 +1,177 @@
package com.yandex.div.compose
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
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.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.test.data.action
import com.yandex.div.test.data.data
import com.yandex.div.test.data.expression
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.variable
import com.yandex.div2.DivData
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class DivViewRecompositionTest {
@get:Rule
val rule = createAndroidComposeRule<ComponentActivity>()
private val mutableData = mutableStateOf(data(text(text = "")))
private val activity: ComponentActivity
get() = rule.activity
private lateinit var divContext: DivContext
@Before
fun setup() {
divContext = DivComposeConfiguration(
reporter = TestReporter()
).createContext(baseContext = activity)
}
@Test
fun `content changes when data is updated`() {
mutableData.value = data(text(text = "Data 1"))
setContent(mutableData)
rule.onNodeWithText("Data 1").assertIsDisplayed()
mutableData.value = data(text(text = "Data 2"))
rule.onNodeWithText("Data 2").assertIsDisplayed()
}
@Test
fun `new variables are used when data is updated`() {
mutableData.value = data(
text(text = expression("Data @{counter}")),
variables = listOf(variable("counter", 1))
)
setContent(mutableData)
rule.onNodeWithText("Data 1").assertIsDisplayed()
mutableData.value = data(
text(text = expression("Data @{counter}")),
variables = listOf(variable("counter", 2))
)
rule.onNodeWithText("Data 2").assertIsDisplayed()
}
@Test
fun `variable values are restored when data is switched back`() {
val data = data(
text(
action = action(
typed = setVariableAction(name = "counter", value = typedValue(5))
),
id = "button",
text = expression("counter = @{counter}")
),
variables = listOf(variable("counter", 1))
)
mutableData.value = data
setContent(mutableData)
rule.onNodeWithTag("button").apply {
assertTextEquals("counter = 1")
performClick()
assertTextEquals("counter = 5")
}
mutableData.value = data(text(text = "New Data"))
rule.onNodeWithText("New Data").assertIsDisplayed()
mutableData.value = data
rule.onNodeWithTag("button").assertTextEquals("counter = 5")
}
@Test
fun `variable values are restored when ComposeView is recreated with the same context`() {
val data = data(
text(
action = action(
typed = setVariableAction(name = "counter", value = typedValue(5))
),
id = "button",
text = expression("counter = @{counter}")
),
variables = listOf(variable("counter", 1))
)
setContent(data)
rule.onNodeWithTag("button").apply {
assertTextEquals("counter = 1")
performClick()
assertTextEquals("counter = 5")
}
activity.setContentView(View(activity))
setContent(data)
rule.onNodeWithTag("button").assertTextEquals("counter = 5")
}
@Test
fun `variable values are not restored when ComposeView is recreated and context is cleared`() {
val data = data(
text(
action = action(
typed = setVariableAction(name = "counter", value = typedValue(5))
),
id = "button",
text = expression("counter = @{counter}")
),
variables = listOf(variable("counter", 1))
)
setContent(data)
rule.onNodeWithTag("button").apply {
assertTextEquals("counter = 1")
performClick()
assertTextEquals("counter = 5")
}
activity.setContentView(View(activity))
divContext.clearViewContext(data)
setContent(data)
rule.onNodeWithTag("button").assertTextEquals("counter = 1")
}
private fun setContent(data: DivData) = setContent(mutableStateOf(data))
private fun setContent(data: State<DivData>) {
activity.setContentView(
ComposeView(divContext).apply {
setContent {
DivView(data.value)
}
}
)
}
}
@@ -1,7 +1,5 @@
package com.yandex.div.compose
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
@@ -150,11 +148,9 @@ class DivViewTest {
content: Div,
variables: List<DivVariable>? = null
) {
rule.setContent {
val divContext = configuration.createContext(baseContext = LocalContext.current)
CompositionLocalProvider(LocalContext provides divContext) {
DivView(data = data(content, variables = variables))
}
}
rule.setContent(
configuration = configuration,
data = data(content, variables = variables)
)
}
}
@@ -1,7 +1,10 @@
package com.yandex.div.compose
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.test.assertTextEquals
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.yandex.div.core.expression.variables.DivVariableController
@@ -9,12 +12,18 @@ import com.yandex.div.data.Variable
import com.yandex.div.test.data.action
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.trigger
import com.yandex.div.test.data.typedValue
import com.yandex.div.test.data.variable
import com.yandex.div2.Div
import com.yandex.div2.DivData
import com.yandex.div2.DivTrigger
import com.yandex.div2.DivVariable
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -23,14 +32,22 @@ import org.junit.runner.RunWith
class DivViewWithTriggersTest {
@get:Rule
val rule = createComposeRule()
val rule = createAndroidComposeRule<ComponentActivity>()
private val variableController = DivVariableController()
private val configuration = DivComposeConfiguration(
reporter = TestReporter(),
variableController = variableController
)
private val activity: ComponentActivity
get() = rule.activity
private lateinit var divContext: DivContext
@Before
fun setup() {
divContext = DivComposeConfiguration(
reporter = TestReporter(),
variableController = variableController
).createContext(baseContext = activity)
}
@Test
fun `text changes when trigger is triggered`() {
@@ -84,14 +101,115 @@ class DivViewWithTriggersTest {
rule.onNodeWithTag("title").assertTextEquals("new text")
}
@Test
fun `does not trigger after leaving composition`() {
val condition = Variable.BooleanVariable("condition", false)
val counter = Variable.IntegerVariable("counter", 1)
variableController.declare(condition, counter)
setContent(
text(
id = "title",
text = expression("counter = @{counter}")
),
triggers = listOf(
trigger(
action = action(
typed = setVariableAction(
name = "counter",
value = typedValue(intExpression("@{counter + 1}"))
)
),
condition = "@{condition}"
)
)
)
activity.setContentView(View(activity))
condition.set(true)
assertEquals(1L, counter.getValue())
}
@Test
fun `does not trigger (local) after leaving composition`() {
val condition = Variable.BooleanVariable("condition", false)
val counter = Variable.IntegerVariable("counter", 1)
variableController.declare(condition, counter)
setContent(
text(
id = "title",
text = expression("counter = @{counter}"),
triggers = listOf(
trigger(
action = action(
typed = setVariableAction(
name = "counter",
value = typedValue(intExpression("@{counter + 1}"))
)
),
condition = "@{condition}"
)
)
)
)
activity.setContentView(View(activity))
condition.set(true)
assertEquals(1L, counter.getValue())
}
@Test
fun `does not trigger when reentering composition and condition is not changed`() {
val condition = Variable.BooleanVariable("condition", false)
val counter = Variable.IntegerVariable("counter", 1)
variableController.declare(condition, counter)
val data = data(
text(
id = "title",
text = expression("counter = @{counter}")
),
triggers = listOf(
trigger(
action = action(
typed = setVariableAction(
name = "counter",
value = typedValue(intExpression("@{counter + 1}"))
)
),
condition = "@{condition}"
)
)
)
setContent(data)
condition.set(true)
assertEquals(2L, counter.getValue())
activity.setContentView(View(activity))
setContent(data)
assertEquals(2L, counter.getValue())
}
private fun setContent(
content: Div,
triggers: List<DivTrigger>? = null,
variables: List<DivVariable>? = null
) {
rule.setContent(
configuration = configuration,
data = data(content, triggers = triggers, variables = variables)
setContent(data(content, triggers = triggers, variables = variables))
}
private fun setContent(data: DivData) {
activity.setContentView(
ComposeView(divContext).apply {
setContent {
DivView(data)
}
}
)
}
}
@@ -45,11 +45,11 @@ class IntegrationTest(testCaseParsingResult: ParsingResult<IntegrationTestCase>)
}
testCase.parseActions().forEach {
divContext.debugFeatures.performAction(it)
divContext.debugFeatures.performAction(data = divData, action = it)
}
testCase.checkResult(
expressionResolver = divContext.debugFeatures.expressionResolver!!
expressionResolver = divContext.debugFeatures.getExpressionResolver(divData)!!
)
}
@@ -118,6 +118,38 @@ class DivTriggerStorageTest {
verifyTriggered()
}
@Test
fun `not triggered when added after stopObserving() is called`() {
triggerStorage.stopObserving()
triggerStorage.add(trigger(action = action, condition = "@{true}"))
verifyNoInteractions(actionHandler)
}
@Test
fun `triggered when startObserving() is called`() {
triggerStorage.stopObserving()
triggerStorage.add(trigger(action = action, condition = "@{true}"))
verifyNoInteractions(actionHandler)
triggerStorage.startObserving()
verifyTriggered()
}
@Test
fun `not triggered when startObserving() is called and was triggered before`() {
triggerStorage.add(trigger(action = action, condition = "@{true}"))
verifyTriggered()
triggerStorage.stopObserving()
triggerStorage.startObserving()
verifyNoInteractions(actionHandler)
}
private fun verifyTriggered() {
verify(actionHandler).handle(actionHandlingContext, action)
clearInvocations(actionHandler)
@@ -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.views.DivLocalContext
import com.yandex.div.compose.views.LocalDivContext
import com.yandex.div.compose.context.DivLocalContext
import com.yandex.div.compose.context.LocalDivContext
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.Variable
import com.yandex.div.json.expressions.Expression
@@ -36,6 +36,7 @@ class ExpressionUtilsTest {
variableController = variableController
),
functionProvider = mock(),
triggerStorage = mock(),
variableController = variableController
)
@@ -28,6 +28,7 @@ adb shell am start -n com.yandex.divkit.demo/com.yandex.divkit.demo.screenshot.D
class DivComposeScreenshotActivity : ComponentActivity() {
private lateinit var divContext: DivContext
private lateinit var data: DivData
val imageLoadingTracker = ComposeImageLoadingTracker()
@@ -55,7 +56,7 @@ class DivComposeScreenshotActivity : ComponentActivity() {
val environment = DivParsingEnvironment(ParsingErrorLogger.ASSERT).apply {
if (templatesJson != null) parseTemplates(templatesJson)
}
val data = DivData(environment, json.getJSONObject("card"))
data = DivData(environment, json.getJSONObject("card"))
val view = ComposeView(divContext).apply {
id = R.id.screenshot_view
@@ -73,7 +74,7 @@ class DivComposeScreenshotActivity : ComponentActivity() {
fun performActions(actions: List<DivAction>) {
actions.forEach {
divContext.debugFeatures.performAction(it)
divContext.debugFeatures.performAction(data = data, action = it)
}
}
@@ -1,5 +1,6 @@
package com.yandex.div.test.data
import com.yandex.div.json.expressions.Expression
import com.yandex.div2.ArrayValue
import com.yandex.div2.ColorValue
import com.yandex.div2.DivTypedValue
@@ -15,6 +16,10 @@ fun typedValue(value: Long): DivTypedValue {
return DivTypedValue.Integer(IntegerValue(value = constant(value)))
}
fun typedValue(value: Expression<Long>): DivTypedValue {
return DivTypedValue.Integer(IntegerValue(value = value))
}
fun typedValue(value: JSONArray): DivTypedValue {
return DivTypedValue.Array(ArrayValue(value = constant(value)))
}