Added compose integration tests

commit_hash:b8423b44ec162dc4882e3be7cf8fc61fab2acbb3
This commit is contained in:
pkurchatov
2026-03-25 14:42:39 +03:00
parent b80ce9d8f0
commit fa8c8bbbf4
28 changed files with 487 additions and 341 deletions
+5 -4
View File
@@ -622,7 +622,7 @@
"client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/Names.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/dagger/Names.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolver.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/DivComposeExpressionResolver.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/EvaluatorWarningSender.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/expressions/EvaluatorWarningSender.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivActionPerformer.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivActionPerformer.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/DivDebugFeatures.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/ImageLoaderProvider.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/internal/ImageLoaderProvider.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/triggers/DivTriggerStorage.kt",
"client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Alignment.kt":"divkit/public/client/android/compose/src/main/kotlin/com/yandex/div/compose/utils/Alignment.kt",
@@ -660,6 +660,7 @@
"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/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/IntegrationTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/IntegrationTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/TestReporter.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/TestReporter.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/actions/DivActionHandlerTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/actions/DivActionHandlerTest.kt",
"client/android/compose/src/test/kotlin/com/yandex/div/compose/actions/SetVariableActionHandlerTest.kt":"divkit/public/client/android/compose/src/test/kotlin/com/yandex/div/compose/actions/SetVariableActionHandlerTest.kt",
@@ -1723,7 +1724,6 @@
"client/android/div/src/test/java/com/yandex/div/core/widget/GridContainerDsl.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/widget/GridContainerDsl.kt",
"client/android/div/src/test/java/com/yandex/div/core/widget/GridContainerTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/core/widget/GridContainerTest.kt",
"client/android/div/src/test/java/com/yandex/div/interactive/IntegrationMultiplatformTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/interactive/IntegrationMultiplatformTest.kt",
"client/android/div/src/test/java/com/yandex/div/interactive/IntegrationTestLogger.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/interactive/IntegrationTestLogger.kt",
"client/android/div/src/test/java/com/yandex/div/internal/storage/DataStorageTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/internal/storage/DataStorageTest.kt",
"client/android/div/src/test/java/com/yandex/div/internal/viewpool/ProfilingSessionExtensionTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/internal/viewpool/ProfilingSessionExtensionTest.kt",
"client/android/div/src/test/java/com/yandex/div/internal/widget/AutoEllipsizeHelperTest.kt":"divkit/public/client/android/div/src/test/java/com/yandex/div/internal/widget/AutoEllipsizeHelperTest.kt",
@@ -17181,7 +17181,8 @@
"client/android/settings.gradle":"divkit/public/client/android/settings.gradle",
"client/android/test-utils/build.gradle.kts":"divkit/public/client/android/test-utils/build.gradle.kts",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestCase.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestCase.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/MultiplatformTestUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/MultiplatformTestUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestCaseParser.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestCaseParser.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestLogger.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/IntegrationTestLogger.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/ParsingResult.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/ParsingResult.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/ParsingUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/crossplatform/ParsingUtils.kt",
"client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivActionUtils.kt":"divkit/public/client/android/test-utils/src/main/kotlin/com/yandex/div/test/data/DivActionUtils.kt",
@@ -26607,7 +26608,7 @@
"test_data/integration_test_data/properties/property_color_value_from_dict.json":"divkit/public/test_data/integration_test_data/properties/property_color_value_from_dict.json",
"test_data/integration_test_data/properties/property_cycle.json":"divkit/public/test_data/integration_test_data/properties/property_cycle.json",
"test_data/integration_test_data/properties/property_integer_value_from_dict.json":"divkit/public/test_data/integration_test_data/properties/property_integer_value_from_dict.json",
"test_data/integration_test_data/properties/property_new_value_varaible_name.json":"divkit/public/test_data/integration_test_data/properties/property_new_value_varaible_name.json",
"test_data/integration_test_data/properties/property_new_value_variable_name.json":"divkit/public/test_data/integration_test_data/properties/property_new_value_variable_name.json",
"test_data/integration_test_data/properties/property_number_value_from_dict.json":"divkit/public/test_data/integration_test_data/properties/property_number_value_from_dict.json",
"test_data/integration_test_data/properties/property_string_value_and_variable_from_dict.json":"divkit/public/test_data/integration_test_data/properties/property_string_value_and_variable_from_dict.json",
"test_data/integration_test_data/properties/property_string_value_from_dict.json":"divkit/public/test_data/integration_test_data/properties/property_string_value_from_dict.json",
+1
View File
@@ -38,4 +38,5 @@ dependencies {
testImplementation(project(":test-utils"))
testImplementation(libs.androidx.compose.ui.test.junit4)
testImplementation(libs.androidx.compose.ui.test.manifest)
testImplementation(libs.json)
}
@@ -4,8 +4,8 @@ import android.content.ContextWrapper
import androidx.annotation.MainThread
import androidx.annotation.VisibleForTesting
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.compose.internal.DivActionPerformer
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.core.annotations.PublicApi
import com.yandex.div.core.expression.variables.DivVariableController
@@ -20,8 +20,8 @@ class DivContext @Inject @MainThread internal constructor(
@InternalApi
@VisibleForTesting
val actionPerformer: DivActionPerformer
get() = component.actionPerformer
val debugFeatures: DivDebugFeatures
get() = component.debugFeatures
internal fun createLocalContext(
variableController: DivVariableController,
@@ -42,8 +42,6 @@ class DivContext @Inject @MainThread internal constructor(
localComponent.triggerStorage.add(it)
}
val context = localComponent.context
actionPerformer.actionHandlingContext = context.actionHandlingContext
return context
return localComponent.context
}
}
@@ -6,7 +6,7 @@ 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.internal.DivActionPerformer
import com.yandex.div.compose.internal.DivDebugFeatures
import com.yandex.yatagan.BindsInstance
import com.yandex.yatagan.Component
import javax.inject.Named
@@ -21,10 +21,10 @@ import javax.inject.Named
internal interface DivContextComponent {
val actionHandler: DivActionHandler
val actionPerformer: DivActionPerformer
val baseContext: Context
val reporter: DivReporter
val debugFeatures: DivDebugFeatures
val imageLoader: ImageLoader
val reporter: DivReporter
@get:Named(Names.HOST_VARIABLES)
val variableController: DivVariableController
@@ -15,6 +15,7 @@ import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.internal.parser.Converter
import com.yandex.div.internal.parser.TypeHelper
import com.yandex.div.internal.parser.ValueValidator
import com.yandex.div.internal.variables.variableValueToEvaluableValue
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.json.invalidValue
@@ -37,7 +38,9 @@ internal class DivComposeExpressionResolver @Inject constructor(
init {
val evaluationContext = EvaluationContext(
variableProvider = { name -> variableController.get(name)?.getValue() },
variableProvider = { name ->
variableController.get(name)?.getValue().variableValueToEvaluableValue()
},
storedValueProvider = { _ -> null },
functionProvider = GeneratedBuiltinFunctionProvider,
warningSender = EvaluatorWarningSender(reporter)
@@ -1,22 +0,0 @@
package com.yandex.div.compose.internal
import com.yandex.div.compose.actions.DivActionHandler
import com.yandex.div.compose.actions.DivActionHandlingContext
import com.yandex.div.compose.dagger.DivContextScope
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div2.DivAction
import javax.inject.Inject
@DivContextScope
@InternalApi
class DivActionPerformer @Inject internal constructor(
private val actionHandler: DivActionHandler
) {
internal var actionHandlingContext: DivActionHandlingContext? = null
fun perform(action: DivAction) {
actionHandlingContext?.let {
actionHandler.handle(context = it, action = action)
}
}
}
@@ -0,0 +1,26 @@
package com.yandex.div.compose.internal
import com.yandex.div.compose.actions.DivActionHandler
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 javax.inject.Inject
@DivContextScope
@InternalApi
class DivDebugFeatures @Inject internal constructor(
private val actionHandler: DivActionHandler
) {
internal var lastViewLocalContext: DivLocalContext? = null
val expressionResolver: ExpressionResolver?
get() = lastViewLocalContext?.expressionResolver
fun performAction(action: DivAction) {
lastViewLocalContext?.let {
actionHandler.handle(context = it.actionHandlingContext, action = action)
}
}
}
@@ -33,7 +33,9 @@ internal fun WithLocalDivContext(data: DivData, content: @Composable () -> Unit)
variableController = DivVariableController(divContext.component.variableController),
triggers = data.variableTriggers.orEmpty(),
variables = data.variables.orEmpty()
)
).also {
divContext.debugFeatures.lastViewLocalContext = it
}
}
CompositionLocalProvider(LocalDivContext provides localContext, content)
}
@@ -0,0 +1,110 @@
package com.yandex.div.compose
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.test.crossplatform.IntegrationTestCase
import com.yandex.div.test.crossplatform.IntegrationTestCaseParser
import com.yandex.div.test.crossplatform.ParsingResult
import com.yandex.div.test.crossplatform.ParsingUtils
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
@RunWith(ParameterizedRobolectricTestRunner::class)
class IntegrationTest(testCaseParsingResult: ParsingResult<IntegrationTestCase>) {
private val testCase = testCaseParsingResult.getOrThrow()
@get:Rule
val rule = createComposeRule()
@Test
fun run() {
val divData = testCase.parseDivData() ?: return
val variableController = DivVariableController()
val divContext = DivComposeConfiguration(
reporter = TestReporter(),
variableController = variableController
).createContext(baseContext = getApplicationContext())
testCase.declareResultVariables(
variables = divData.variables ?: emptyList(),
variableController = variableController
)
rule.setContent {
CompositionLocalProvider(LocalContext provides divContext) {
DivView(data = divData)
}
}
testCase.parseActions().forEach {
divContext.debugFeatures.performAction(it)
}
testCase.checkResult(
expressionResolver = divContext.debugFeatures.expressionResolver!!
)
}
companion object {
// Store parsed test cases to prevent multiple parsing by
// ParameterizedRobolectricTestRunner
private val cases: List<ParsingResult<IntegrationTestCase>> = run {
ParsingUtils.parseFiles("integration_test_data") { file, json ->
if (ignoredFiles.contains(file.name)) {
emptyList()
} else {
IntegrationTestCaseParser.parseCases(file.name, json)
}
}
}
@JvmStatic
@Suppress("unused")
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun cases() = cases
}
}
private val ignoredFiles = listOf(
"array_variable_mutation.json",
"decl_expressions_item_builder.json",
"decl_expressions_item_builder_override.json",
"dict_set_value.json",
"expression_with_several_local_functions.json",
"item_builder_variable_triggers.json",
"local_functions_array.json",
"local_functions_datetime.json",
"local_functions_color.json",
"local_functions_dict.json",
"local_functions_div_data.json",
"local_functions_int.json",
"local_functions_number.json",
"local_functions_string.json",
"local_functions_url.json",
"local-triggers-gallery.json",
"local-triggers-gallery-with-item-builder.json",
"local-triggers-states.json",
"local-triggers-tabs.json",
"properties_cycled.json",
"property_boolean_value_from_dict.json",
"property_color_value_from_array.json",
"property_color_value_from_dict.json",
"property_cycle.json",
"property_integer_value_from_dict.json",
"property_new_value_variable_name.json",
"property_number_value_from_dict.json",
"property_string_value_and_variable_from_dict.json",
"property_string_value_from_dict.json",
"property_string_value_from_variable.json",
"property_url_value_from_dict.json",
"property_without_setter.json",
"update_structure.json",
"wrap_content_constraints_warning.json",
)
@@ -1,7 +1,9 @@
package com.yandex.div.internal.variables
import android.net.Uri
import com.yandex.div.core.annotations.InternalApi
import com.yandex.div.data.Variable
import com.yandex.div.evaluable.types.Url
import com.yandex.div.internal.data.PropertyDelegate
import com.yandex.div.internal.data.PropertyVariableExecutor
import com.yandex.div.internal.expressions.DivExpressionParser.readTypedExpression
@@ -71,6 +73,34 @@ fun PropertyVariable.parseGet(
return expression
}
@InternalApi
val DivVariable.name: String
get() {
return when (this) {
is DivVariable.Bool -> this.value.name
is DivVariable.Integer -> this.value.name
is DivVariable.Number -> this.value.name
is DivVariable.Str -> this.value.name
is DivVariable.Color -> this.value.name
is DivVariable.Url -> this.value.name
is DivVariable.Dict -> this.value.name
is DivVariable.Array -> this.value.name
is DivVariable.Property -> this.value.name
}
}
/**
* Converts [DivVariable] value to the value that can be used in
* [com.yandex.div.evaluable.VariableProvider].
*/
@InternalApi
fun Any?.variableValueToEvaluableValue(): Any? {
return when(this) {
is Uri -> Url(toString())
else -> this
}
}
private fun PropertyVariable.toVariable(
resolver: ExpressionResolver,
propertyVariableExecutor: PropertyVariableExecutor,
@@ -8,4 +8,4 @@ fun interface VariableProvider {
* @return variable value or null if it is missing.
*/
fun get(name: String): Any?
}
}
@@ -2,8 +2,6 @@ package com.yandex.div.core.actions
import com.yandex.div.core.DivActionHandler
import com.yandex.div.core.DivRequestExecutor
import com.yandex.div.core.expression.getWrappedValue
import com.yandex.div.core.expression.name
import com.yandex.div.core.state.DivStatePath
import com.yandex.div.core.view2.BindingContext
import com.yandex.div.core.view2.Div2View
@@ -11,6 +9,8 @@ import com.yandex.div.evaluable.MissingVariableException
import com.yandex.div.internal.core.DivItemBuilderResult
import com.yandex.div.internal.core.DivTreeVisitor
import com.yandex.div.internal.core.toItemBuilderResult
import com.yandex.div.internal.variables.name
import com.yandex.div.internal.variables.variableValueToEvaluableValue
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.Div
import com.yandex.div2.DivAction
@@ -60,7 +60,7 @@ class DivActionTypedSubmitHandler @Inject constructor(
variables.forEach {
val name = it.name
container.expressionResolver.getVariable(name)?.let {
variable -> body.put(name, variable.getWrappedValue())
variable -> body.put(name, variable.getValue().variableValueToEvaluableValue())
} ?: view.logError(MissingVariableException(name))
}
return body.toString()
@@ -13,6 +13,7 @@ import com.yandex.div.core.view2.errors.ErrorCollector
import com.yandex.div.core.view2.errors.ErrorCollectors
import com.yandex.div.data.Variable
import com.yandex.div.internal.data.PropertyVariableExecutor
import com.yandex.div.internal.variables.name
import com.yandex.div.internal.variables.parseGet
import com.yandex.div2.DivData
import com.yandex.div2.DivVariable
@@ -125,18 +126,3 @@ internal class RuntimeStoreProvider @Inject constructor(
}
}
}
internal val DivVariable.name: String
get() {
return when (this) {
is DivVariable.Bool -> this.value.name
is DivVariable.Integer -> this.value.name
is DivVariable.Number -> this.value.name
is DivVariable.Str -> this.value.name
is DivVariable.Color -> this.value.name
is DivVariable.Url -> this.value.name
is DivVariable.Dict -> this.value.name
is DivVariable.Array -> this.value.name
is DivVariable.Property -> this.value.name
}
}
@@ -1,9 +1,5 @@
package com.yandex.div.core.expression
import com.yandex.div.core.expression.variables.wrapVariableValue
import com.yandex.div.data.Variable
import com.yandex.div.json.expressions.ExpressionResolver
internal val ExpressionResolver.asImpl get() = this as? ExpressionResolverImpl
internal fun Variable.getWrappedValue() = getValue().wrapVariableValue()
@@ -4,6 +4,7 @@ import com.yandex.div.core.Disposable
import com.yandex.div.core.view2.errors.ErrorCollector
import com.yandex.div.data.Variable
import com.yandex.div.internal.variables.VariableSource
import com.yandex.div.internal.variables.variableValueToEvaluableValue
import com.yandex.div.json.expressions.ExpressionResolver
internal class VariableAndConstantController(
@@ -11,7 +12,9 @@ internal class VariableAndConstantController(
private val constants: ConstantsProvider,
) : VariableController {
override fun get(name: String) = constants.get(name).wrapVariableValue() ?: delegate.get(name)
override fun get(name: String): Any? {
return constants.get(name).variableValueToEvaluableValue() ?: delegate.get(name)
}
override fun subscribeToVariablesChange(
names: List<String>,
@@ -1,12 +1,10 @@
package com.yandex.div.core.expression.variables
import android.net.Uri
import com.yandex.div.core.Disposable
import com.yandex.div.core.view2.errors.ErrorCollector
import com.yandex.div.data.Variable
import com.yandex.div.data.VariableDeclarationException
import com.yandex.div.evaluable.VariableProvider
import com.yandex.div.evaluable.types.Url
import com.yandex.div.internal.data.PropertyVariableExecutor
import com.yandex.div.internal.variables.VariableSource
import com.yandex.div.internal.variables.toVariable
@@ -48,11 +46,6 @@ internal interface VariableController : VariableProvider {
fun captureAll(): List<Variable> = emptyList()
}
internal fun Any?.wrapVariableValue() = when(this) {
is Uri -> Url(this.toString())
else -> this
}
internal fun VariableController.declare(
divVariable: DivVariable,
resolver: ExpressionResolver,
@@ -11,6 +11,7 @@ import com.yandex.div.data.VariableDeclarationException
import com.yandex.div.internal.util.UiThreadHandler
import com.yandex.div.internal.variables.DeclarationObserver
import com.yandex.div.internal.variables.VariableSource
import com.yandex.div.internal.variables.variableValueToEvaluableValue
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div.json.missingVariable
import java.util.Collections
@@ -42,7 +43,9 @@ internal class VariableControllerImpl(
observers.addObserver(observer)
}
override fun get(name: String) = getMutableVariable(name)?.getValue().wrapVariableValue() ?: delegate?.get(name)
override fun get(name: String): Any? {
return getMutableVariable(name)?.getValue().variableValueToEvaluableValue() ?: delegate?.get(name)
}
override fun subscribeToVariablesChange(
names: List<String>,
@@ -15,8 +15,8 @@ import com.yandex.div.internal.util.UiThreadHandler
import com.yandex.div.internal.util.map
import com.yandex.div.json.ParsingErrorLogger
import com.yandex.div.rule.LocaleRule
import com.yandex.div.test.crossplatform.MultiplatformTestUtils
import com.yandex.div.test.crossplatform.ParsingResult
import com.yandex.div.test.crossplatform.ParsingUtils
import org.json.JSONArray
import org.json.JSONObject
import org.junit.After
@@ -78,7 +78,11 @@ class EvaluableMultiplatformTest(
is ParsingResult.Error -> caseParsingResult.throwException()
}
val testDivData = createDivDataFromTestVars(testCase.variables, testCase.functions, testParsingLogger)
val testDivData = createDivDataFromTestVars(
testCase.variables,
testCase.functions,
testParsingLogger
)
runtimeProvider = ExpressionsRuntimeProvider(
mockDivVariableController,
@@ -114,7 +118,7 @@ class EvaluableMultiplatformTest(
}
is JSONArray, is JSONObject -> {
if (testCase.expectedType == VALUE_TYPE_UNORDERED_ARRAY){
if (testCase.expectedType == VALUE_TYPE_UNORDERED_ARRAY) {
checkEquality(testCase) { message, expected, actual ->
val expectedList = (expected as JSONArray).map { toString() }.sorted()
val actualList = (actual as JSONArray).map { toString() }.sorted()
@@ -146,7 +150,8 @@ class EvaluableMultiplatformTest(
if (evalExpression is Throwable) {
throw AssertionError(
"Expecting '${testCase.expectedValue}' at expression '${testCase.expression}' " +
"but got exception instead!", evalExpression)
"but got exception instead!", evalExpression
)
}
validate("expression: '${testCase.expression}'", testCase.expectedValue, evalExpression)
}
@@ -173,21 +178,14 @@ class EvaluableMultiplatformTest(
companion object {
private const val TEST_CASES_FILE_PATH = "expression_test_data"
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun cases(): List<ParsingResult<ExpressionTestCase>> {
val cases = mutableListOf<ParsingResult<ExpressionTestCase>>()
val errors = MultiplatformTestUtils
.walkJSONs(TEST_CASES_FILE_PATH) { file, jsonString ->
val newCases = ExpressionTestCaseUtils.parseTestCases(JSONObject(jsonString), file.name)
cases.addAll(newCases)
}
val allCases = errors + cases
ExpressionTestCaseUtils.checkDuplicates(allCases.asSequence())
return allCases
val cases = ParsingUtils.parseFiles("expression_test_data") { file, json ->
ExpressionTestCaseUtils.parseTestCases(JSONObject(json), file.name)
}
ExpressionTestCaseUtils.checkDuplicates(cases.asSequence())
return cases
}
}
}
@@ -1,8 +1,6 @@
package com.yandex.div.core.expression
import android.net.Uri
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.data.Variable
import com.yandex.div.evaluable.EvaluableException
import com.yandex.div.evaluable.types.Color
import com.yandex.div.evaluable.types.Url
@@ -18,7 +16,6 @@ import com.yandex.div2.DivData
import com.yandex.div2.DivEvaluableType
import com.yandex.div2.DivFunction
import com.yandex.div2.DivVariable
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -32,8 +29,8 @@ object ExpressionTestCaseUtils {
private const val VALUE_TYPE_DATE_TIME = "datetime"
private const val VALUE_TYPE_URL = "url"
private const val VALUE_TYPE_COLOR = "color"
const val VALUE_TYPE_DICT = "dict"
const val VALUE_TYPE_ARRAY = "array"
private const val VALUE_TYPE_DICT = "dict"
private const val VALUE_TYPE_ARRAY = "array"
const val VALUE_TYPE_UNORDERED_ARRAY = "unordered_array"
private const val VALUE_TYPE_UNIT = "unit"
private const val VALUE_TYPE_ERROR = "error"
@@ -113,21 +110,7 @@ object ExpressionTestCaseUtils {
return value
}
val JSONObject.type: String get() = getString(TYPE_FIELD)
fun createVariable(type: String, name: String, value: Any?): Variable {
return when (type) {
VALUE_TYPE_STRING -> Variable.StringVariable(name, value as String? ?: "")
VALUE_TYPE_INTEGER -> Variable.IntegerVariable(name, value as Long? ?: 0)
VALUE_TYPE_DECIMAL -> Variable.DoubleVariable(name, value as Double? ?: 0.0)
VALUE_TYPE_BOOLEAN -> Variable.BooleanVariable(name, ANY_TO_BOOLEAN(value ?: false))
VALUE_TYPE_COLOR -> Variable.ColorVariable(name, (value as Color?)?.value ?: 0)
VALUE_TYPE_URL -> Variable.UrlVariable(name, value as Uri? ?: Uri.EMPTY)
VALUE_TYPE_DICT -> Variable.DictVariable(name, value as JSONObject? ?: JSONObject())
VALUE_TYPE_ARRAY -> Variable.ArrayVariable(name, value as JSONArray? ?: JSONArray())
else -> throw IllegalAccessException("Unknown variable type: $type")
}
}
private val JSONObject.type: String get() = getString(TYPE_FIELD)
fun createDivDataFromTestVars(
vars: List<JSONObject>,
@@ -5,8 +5,8 @@ import com.yandex.div.evaluable.EvaluableType
import com.yandex.div.evaluable.FunctionArgument
import com.yandex.div.evaluable.function.GeneratedBuiltinFunctionProvider
import com.yandex.div.test.crossplatform.isForAndroid
import com.yandex.div.test.crossplatform.MultiplatformTestUtils
import com.yandex.div.test.crossplatform.ParsingResult
import com.yandex.div.test.crossplatform.ParsingUtils
import com.yandex.div.test.crossplatform.toObjectList
import org.json.JSONException
import org.json.JSONObject
@@ -63,18 +63,14 @@ class SignaturesMultiplatformTest(testCaseParsingResult: ParsingResult<Signature
@JvmStatic
@Parameterized.Parameters(name = "{0}")
fun signatures(): List<ParsingResult<SignatureTestCase>> {
val cases = mutableListOf<ParsingResult<SignatureTestCase>>()
val errors = MultiplatformTestUtils
.walkJSONs(File(SIGNATURES_DIR_PATH)) { file, jsonString ->
val newCases = JSONObject(jsonString)
.optJSONArray(FIELD_SIGNATURE)
.toObjectList()
.filter { it.isForAndroid }
.map { parseSignature(file, it) }
.filterIsInstance<ParsingResult.Success<SignatureTestCase>>()
cases.addAll(newCases)
}
return cases + errors
return ParsingUtils.parseFiles(File(SIGNATURES_DIR_PATH)) { file, json ->
JSONObject(json)
.optJSONArray(FIELD_SIGNATURE)
.toObjectList()
.filter { it.isForAndroid }
.map { parseSignature(file, it) }
.filterIsInstance<ParsingResult.Success<SignatureTestCase>>()
}
}
private fun parseSignature(file: File, json: JSONObject): ParsingResult<SignatureTestCase> {
@@ -97,12 +93,14 @@ class SignaturesMultiplatformTest(testCaseParsingResult: ParsingResult<Signature
return ParsingResult.Error(file.name, json, e)
}
val isMethod = json.optBoolean(FIELD_SIGNATURE_IS_METHOD)
return ParsingResult.Success(SignatureTestCase(
"$functionName(${arguments ?: ""}) $resultType",
functionName,
arguments ?: emptyList(),
resultType,
isMethod)
return ParsingResult.Success(
SignatureTestCase(
"$functionName(${arguments ?: ""}) $resultType",
functionName,
arguments ?: emptyList(),
resultType,
isMethod
)
)
}
}
@@ -5,23 +5,13 @@ import com.yandex.div.DivDataTag
import com.yandex.div.core.Div2Context
import com.yandex.div.core.DivConfiguration
import com.yandex.div.core.actions.observeErrors
import com.yandex.div.core.expression.ExpressionTestCaseUtils.VALUE_TYPE_ARRAY
import com.yandex.div.core.expression.ExpressionTestCaseUtils.VALUE_TYPE_DICT
import com.yandex.div.core.expression.ExpressionTestCaseUtils.createVariable
import com.yandex.div.core.expression.getWrappedValue
import com.yandex.div.core.expression.name
import com.yandex.div.core.expression.variables.wrapVariableValue
import com.yandex.div.core.images.DivImageLoader
import com.yandex.div.core.images.LoadReference
import com.yandex.div.core.view2.Div2View
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.test.crossplatform.IntegrationTestCase
import com.yandex.div.test.crossplatform.MultiplatformTestUtils
import com.yandex.div.test.crossplatform.IntegrationTestCaseParser
import com.yandex.div.test.crossplatform.ParsingResult
import com.yandex.div2.DivAction
import com.yandex.div2.DivData
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import com.yandex.div.test.crossplatform.ParsingUtils
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
@@ -32,100 +22,46 @@ import java.util.UUID
class IntegrationMultiplatformTest(testCaseParsingResult: ParsingResult<IntegrationTestCase>) {
private val testCase = testCaseParsingResult.getOrThrow()
private val expectedResults = testCase.expectedResults
private val activity = Robolectric.buildActivity(Activity::class.java).get()
private val logger = IntegrationTestLogger()
@Test
fun run() {
val env = DivParsingEnvironment(logger)
testCase.divData.optJSONObject("templates")?.let {
env.parseTemplates(it)
}
val divData = runCatching {
DivData(env, testCase.divData.getJSONObject("card"))
}.getOrElse {
var errorIsExpected = false
expectedResults.forEach { e ->
if (e !is IntegrationTestCase.ExpectedResult.Error) return@forEach
checkError(e)
errorIsExpected = true
}
if (!errorIsExpected) {
throw AssertionError("Got unexpected error at data parsing!", it)
}
return
}
val divData = testCase.parseDivData() ?: return
val context = Div2Context(activity, DivConfiguration.Builder(IMAGE_LOADER_STUB).build())
expectedResults.forEach { result ->
when (result) {
is IntegrationTestCase.ExpectedResult.Variable -> {
if (divData.variables?.any { it.name == result.name } != true) {
val variable = createVariable(result.type, result.name, null)
context.divVariableController.declare(variable)
}
}
is IntegrationTestCase.ExpectedResult.Error -> return@forEach
}
}
testCase.declareResultVariables(
variables = divData.variables ?: emptyList(),
variableController = context.divVariableController
)
val divView = Div2View(context)
divView.setData(divData, DivDataTag(UUID.randomUUID().toString()))
divView.observeErrors { errors, _ ->
errors.forEach { error ->
logger.logErrorDirectly(error)
testCase.logger.logErrorDirectly(error)
}
}
testCase.actions.forEach {
runCatching { divView.handleAction(DivAction(env, it)) }
testCase.parseActions().forEach {
divView.handleAction(it)
}
expectedResults.forEach {
when (it) {
is IntegrationTestCase.ExpectedResult.Error -> checkError(it)
is IntegrationTestCase.ExpectedResult.Variable -> {
val expectedValue = it.value.wrapVariableValue()
val actualValue = divView.expressionResolver
.getVariable(it.name)
?.getWrappedValue()
if (it.type == VALUE_TYPE_DICT || it.type == VALUE_TYPE_ARRAY) {
assertEquals(expectedValue.toString(), actualValue.toString())
} else {
assertEquals(expectedValue, actualValue)
}
}
}
}
}
private fun checkError(expected: IntegrationTestCase.ExpectedResult.Error) {
assertTrue(
"Expected: <${expected.message}> but was: <${
logger.messages.toSet().joinToString(", ")
}>",
logger.messages.contains(expected.message)
testCase.checkResult(
expressionResolver = divView.expressionResolver
)
}
companion object {
private const val TEST_CASES_FILE_PATH = "integration_test_data"
private val EMPTY_REF = LoadReference { }
private val IMAGE_LOADER_STUB = DivImageLoader { _, _ -> EMPTY_REF }
// Store parsed test cases to prevent multiple parsing by
// ParameterizedRobolectricTestRunner
private val cases: List<ParsingResult<IntegrationTestCase>> = run {
val cases = mutableListOf<ParsingResult<IntegrationTestCase>>()
val errors = MultiplatformTestUtils
.walkJSONs(TEST_CASES_FILE_PATH) { file, json ->
cases.addAll(IntegrationTestCase.parse(file.name, json))
}
errors + cases
ParsingUtils.parseFiles("integration_test_data") { file, json ->
IntegrationTestCaseParser.parseCases(file.name, json)
}
}
@JvmStatic
@@ -73,7 +73,7 @@ class DivComposeScreenshotActivity : ComponentActivity() {
fun performActions(actions: List<DivAction>) {
actions.forEach {
divContext.actionPerformer.perform(it)
divContext.debugFeatures.performAction(it)
}
}
@@ -1,104 +1,128 @@
package com.yandex.div.test.crossplatform
import androidx.core.net.toUri
import com.yandex.div.evaluable.types.Color
import org.json.JSONException
import android.net.Uri
import com.yandex.div.core.expression.variables.DivVariableController
import com.yandex.div.data.DivParsingEnvironment
import com.yandex.div.data.Variable
import com.yandex.div.internal.variables.name
import com.yandex.div.json.expressions.ExpressionResolver
import com.yandex.div2.DivAction
import com.yandex.div2.DivData
import com.yandex.div2.DivVariable
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
class IntegrationTestCase(
val name: String,
val divData: JSONObject,
val actions: List<JSONObject>,
val expectedResults: List<ExpectedResult>
private val name: String,
private val divData: JSONObject,
private val actions: List<JSONObject>,
private val expectedResults: List<ExpectedResult>
) {
sealed interface ExpectedResult {
class Variable(val name: String, val type: String, val value: Any) : ExpectedResult
class Variable(
val name: String,
val type: String,
private val value: Any
) : ExpectedResult {
fun check(expressionResolver: ExpressionResolver) {
val actualValue = expressionResolver.getVariable(name)?.getValue()
if (type == "array" || type == "dict" || type == "url") {
assertEquals(value.toString(), actualValue.toString())
} else {
assertEquals(value, actualValue)
}
}
}
class Error(val message: String) : ExpectedResult
class Error(private val message: String) : ExpectedResult {
fun check(errors: List<String>) {
assertTrue(
"Expected: <$message> but was: <${errors.toSet().joinToString(", ")}>",
errors.contains(message)
)
}
}
}
val logger = IntegrationTestLogger()
private val parsingEnvironment = DivParsingEnvironment(logger)
override fun toString() = name
companion object {
fun parseDivData(): DivData? {
divData.optJSONObject("templates")?.let {
parsingEnvironment.parseTemplates(it)
}
fun parse(
fileName: String,
jsonString: String
): List<ParsingResult<IntegrationTestCase>> {
val json = JSONObject(jsonString)
return json.getJSONArray("cases")
.toObjectList()
.mapIndexedNotNull { index, jsonObject ->
if (!jsonObject.isForAndroid) {
return@mapIndexedNotNull null
}
try {
val testCase = jsonObject.parseTestCase(
fileName = fileName,
index = index,
// Fresh instance is required for every test case since JSONObject
// may contain mutable elements (array and dict variables).
divData = JSONObject(jsonString).getJSONObject("div_data")
)
ParsingResult.Success(testCase)
} catch (e: JSONException) {
ParsingResult.Error(fileName, json, e)
}
try {
return DivData(parsingEnvironment, divData.getJSONObject("card"))
} catch (throwable: Throwable) {
var isErrorExpected = false
expectedResults
.filterIsInstance<ExpectedResult.Error>()
.forEach { result ->
result.check(logger.messages)
isErrorExpected = true
}
if (!isErrorExpected) {
fail("Unexpected parsing error: ${throwable.message}")
}
}
return null
}
fun parseActions(): List<DivAction> {
return actions.map {
DivAction(json = it, env = parsingEnvironment)
}
}
/**
* Declares variables that are used in expected results but no not declared in DivData.
*/
fun declareResultVariables(
variables: List<DivVariable>,
variableController: DivVariableController
) {
expectedResults
.filterIsInstance<ExpectedResult.Variable>()
.forEach { variable ->
if (!variables.any { it.name == variable.name }) {
variableController.declare(createVariable(variable.type, variable.name))
}
}
}
fun checkResult(expressionResolver: ExpressionResolver) {
expectedResults.forEach {
when (it) {
is ExpectedResult.Error ->
it.check(logger.messages)
is ExpectedResult.Variable ->
it.check(expressionResolver = expressionResolver)
}
}
}
}
private fun JSONObject.parseTestCase(
fileName: String,
index: Int,
divData: JSONObject
): IntegrationTestCase {
val actions = optJSONArray("div_actions").toObjectList()
var name = "$fileName Case $index"
actions.forEach {
name += ", ${it.getString("log_id")}"
}
return IntegrationTestCase(
name = name,
divData = divData,
actions = actions,
expectedResults = getJSONArray("expected")
.toObjectList()
.map { it.parseExpectedResult() }
)
}
private fun JSONObject.parseExpectedResult(): IntegrationTestCase.ExpectedResult {
return when (val type = getString("type")) {
"variable" -> {
val value = getJSONObject("value")
IntegrationTestCase.ExpectedResult.Variable(
name = getString("variable_name"),
type = value.getString("type"),
value = value.getVariableValue()
)
}
"error" -> IntegrationTestCase.ExpectedResult.Error(getString("value"))
else -> throw JSONException("Unknown expected result type: $type")
}
}
private fun JSONObject.getVariableValue(): Any {
return when (val type = getString("type")) {
"array" -> getJSONArray("value")
"boolean" -> get("value")
"color" -> Color.parse(getString("value"))
"datetime" -> parseDateTime(getString("value"))
"dict" -> getJSONObject("value")
"integer" -> getLong("value")
"number" -> getDouble("value")
"string" -> getString("value")
"url" -> getString("value").toUri()
private fun createVariable(type: String, name: String): Variable {
return when (type) {
"array" -> Variable.ArrayVariable(name, JSONArray())
"boolean" -> Variable.BooleanVariable(name, false)
"color" -> Variable.ColorVariable(name, 0)
"dict" -> Variable.DictVariable(name, JSONObject())
"integer" -> Variable.IntegerVariable(name, 0)
"number" -> Variable.DoubleVariable(name, 0.0)
"string" -> Variable.StringVariable(name, "")
"url" -> Variable.UrlVariable(name, Uri.EMPTY)
else -> throw IllegalAccessException("Unknown variable type: $type")
}
}
@@ -0,0 +1,86 @@
package com.yandex.div.test.crossplatform
import com.yandex.div.evaluable.types.Color
import org.json.JSONException
import org.json.JSONObject
object IntegrationTestCaseParser {
fun parseCases(
fileName: String,
jsonString: String
): List<ParsingResult<IntegrationTestCase>> {
val json = JSONObject(jsonString)
return json.getJSONArray("cases")
.toObjectList()
.mapIndexedNotNull { index, jsonObject ->
if (!jsonObject.isForAndroid) {
return@mapIndexedNotNull null
}
try {
val testCase = jsonObject.parseTestCase(
fileName = fileName,
index = index,
// Fresh instance is required for every test case since JSONObject
// may contain mutable elements (array and dict variables).
divData = JSONObject(jsonString).getJSONObject("div_data")
)
ParsingResult.Success(testCase)
} catch (e: Exception) {
ParsingResult.Error(fileName = fileName, error = e)
}
}
}
}
private fun JSONObject.parseTestCase(
fileName: String,
index: Int,
divData: JSONObject
): IntegrationTestCase {
val actions = optJSONArray("div_actions").toObjectList()
var name = "$fileName Case $index"
actions.forEach {
name += ", ${it.getString("log_id")}"
}
return IntegrationTestCase(
name = name,
divData = divData,
actions = actions,
expectedResults = getJSONArray("expected")
.toObjectList()
.map { it.parseExpectedResult() }
)
}
private fun JSONObject.parseExpectedResult(): IntegrationTestCase.ExpectedResult {
return when (val type = getString("type")) {
"variable" -> {
val value = getJSONObject("value")
IntegrationTestCase.ExpectedResult.Variable(
name = getString("variable_name"),
type = value.getString("type"),
value = value.getVariableValue()
)
}
"error" -> IntegrationTestCase.ExpectedResult.Error(getString("value"))
else -> throw JSONException("Unknown expected result type: $type")
}
}
private fun JSONObject.getVariableValue(): Any {
return when (val type = getString("type")) {
"array" -> getJSONArray("value")
"boolean" -> get("value")
"color" -> Color.parse(getString("value"))
"datetime" -> parseDateTime(getString("value"))
"dict" -> getJSONObject("value")
"integer" -> getLong("value")
"number" -> getDouble("value")
"string" -> getString("value")
"url" -> getString("value")
else -> throw IllegalAccessException("Unknown variable type: $type")
}
}
@@ -1,4 +1,4 @@
package com.yandex.div.interactive
package com.yandex.div.test.crossplatform
import com.yandex.div.json.ParsingErrorLogger
@@ -12,11 +12,7 @@ class IntegrationTestLogger : ParsingErrorLogger {
}
fun logErrorDirectly(e: Throwable) {
collectChainMessages(e)
}
private fun collectChainMessages(t: Throwable) {
generateSequence(t) { it.cause }
generateSequence(e) { it.cause }
.mapNotNull { it.message }
.forEach { _messages.add(it) }
}
@@ -1,50 +0,0 @@
package com.yandex.div.test.crossplatform
import org.json.JSONException
import java.io.File
private const val JSON_EXTENSION = "json"
private const val TEST_DATA_PATH = "../../../test_data/"
object MultiplatformTestUtils {
fun walkJSONs(
relativePath: String,
parseAction: (file: File, json: String) -> Unit
): List<ParsingResult.Error> {
return walkJSONs(directory = File(TEST_DATA_PATH, relativePath), parseAction)
}
fun walkJSONs(
directory: File,
parseAction: (file: File, json: String) -> Unit
): List<ParsingResult.Error> {
val errors = mutableListOf<ParsingResult.Error>()
getFiles(directory)
.forEach { file ->
val json = try {
file.readText(Charsets.UTF_8)
} catch (e: Exception) {
errors.add(ParsingResult.Error(fileName = file.name, error = e))
return@forEach
}
try {
parseAction(file, json)
} catch (e: JSONException) {
errors.add(ParsingResult.Error(fileName = file.name, error = e))
}
}
return errors
}
private fun getFiles(dir: File): List<File> {
val (directories, files) = dir.listFiles().orEmpty()
.partition { it.isDirectory }
return arrayListOf<File>().apply {
addAll(files.filter { file -> file.extension == JSON_EXTENSION })
addAll(directories.flatMap { getFiles(it) })
}
}
}
@@ -4,11 +4,56 @@ import com.yandex.div.evaluable.types.DateTime
import com.yandex.div.internal.util.map
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import kotlin.collections.filter
import kotlin.collections.flatMap
import kotlin.collections.forEach
import kotlin.collections.orEmpty
import kotlin.collections.partition
import kotlin.io.extension
private const val TEST_DATA_PATH = "../../../test_data/"
object ParsingUtils {
fun <T : Any> parseFiles(
relativePath: String,
parseAction: (file: File, json: String) -> List<ParsingResult<T>>
): List<ParsingResult<T>> {
return parseFiles(directory = File(TEST_DATA_PATH, relativePath), parseAction)
}
fun <T : Any> parseFiles(
directory: File,
parseAction: (file: File, json: String) -> List<ParsingResult<T>>
): List<ParsingResult<T>> {
val results = mutableListOf<ParsingResult<T>>()
getFiles(directory).forEach { file ->
try {
val json = file.readText(Charsets.UTF_8)
results.addAll(parseAction(file, json))
} catch (e: Exception) {
results.add(ParsingResult.Error(fileName = file.name, error = e))
return@forEach
}
}
return results
}
private fun getFiles(dir: File): List<File> {
val (directories, files) = dir.listFiles().orEmpty()
.partition { it.isDirectory }
return arrayListOf<File>().apply {
addAll(files.filter { file -> file.extension == "json" })
addAll(directories.flatMap { getFiles(it) })
}
}
}
val JSONObject.platforms: List<String>
get() = getJSONArray("platforms").map { it as String }