| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.plugins.kotlin |
| |
| import android.widget.TextView |
| import androidx.compose.FrameManager |
| import androidx.compose.Composer |
| import androidx.compose.currentComposerNonNull |
| import org.junit.Before |
| import org.junit.Ignore |
| import org.junit.Test |
| import org.junit.runner.RunWith |
| import org.robolectric.Robolectric |
| import org.robolectric.RuntimeEnvironment |
| import org.robolectric.annotation.Config |
| |
| val PRESIDENT_NAME_1 = "George Washington" |
| val PRESIDENT_AGE_1 = 57 |
| val PRESIDENT_NAME_16 = "Abraham Lincoln" |
| val PRESIDENT_AGE_16 = 52 |
| |
| @RunWith(ComposeRobolectricTestRunner::class) |
| @Config( |
| manifest = Config.NONE, |
| minSdk = 23, |
| maxSdk = 23 |
| ) |
| class KtxModelCodeGenTests : AbstractCodegenTest() { |
| |
| @Before |
| fun before() { |
| val scheduler = RuntimeEnvironment.getMasterScheduler() |
| scheduler.pause() |
| } |
| |
| override fun setUp() { |
| isSetup = true |
| super.setUp() |
| } |
| |
| private var isSetup = false |
| private inline fun <T> ensureSetup(crossinline block: () -> T): T { |
| if (!isSetup) setUp() |
| return FrameManager.isolated { block() } |
| } |
| |
| @Test |
| @Ignore("TODO(b/138720405): Investigate synchronisation issues in tests") |
| fun testCGModelView_PersonModel(): Unit = ensureSetup { |
| val tvNameId = 384 |
| val tvAgeId = 385 |
| |
| var name = PRESIDENT_NAME_1 |
| var age = PRESIDENT_AGE_1 |
| compose( |
| """ |
| @Model |
| class Person4(var name: String, var age: Int) |
| |
| @Composable |
| fun PersonView4(person: Person4) { |
| Observe { |
| TextView(text=person.name, id=$tvNameId) |
| TextView(text=person.age.toString(), id=$tvAgeId) |
| } |
| } |
| |
| val president = Person4("$PRESIDENT_NAME_1", $PRESIDENT_AGE_1) |
| """, { mapOf("name" to name, "age" to age) }, """ |
| president.name = name |
| president.age = age |
| """, """ |
| PersonView4(person=president) |
| """).then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_1, tvName.text) |
| assertEquals(PRESIDENT_AGE_1.toString(), tvAge.text) |
| |
| name = PRESIDENT_NAME_16 |
| age = PRESIDENT_AGE_16 |
| }.then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_16, tvName.text) |
| assertEquals(PRESIDENT_AGE_16.toString(), tvAge.text) |
| } |
| } |
| |
| @Test // b/120843442 |
| fun testCGModelView_ObjectModel(): Unit = ensureSetup { |
| val tvNameId = 384 |
| val tvAgeId = 385 |
| |
| var name = PRESIDENT_NAME_1 |
| var age = PRESIDENT_AGE_1 |
| compose( |
| """ |
| @Model |
| object president { |
| var name: String = "$PRESIDENT_NAME_1" |
| var age: Int = $PRESIDENT_AGE_1 |
| } |
| |
| @Composable |
| fun PresidentView() { |
| Observe { |
| TextView(text=president.name, id=$tvNameId) |
| TextView(text=president.age.toString(), id=$tvAgeId) |
| } |
| } |
| |
| """, { mapOf("name" to name, "age" to age) }, """ |
| president.name = name |
| president.age = age |
| """, """ |
| PresidentView() |
| """).then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_1, tvName.text) |
| assertEquals(PRESIDENT_AGE_1.toString(), tvAge.text) |
| |
| name = PRESIDENT_NAME_16 |
| age = PRESIDENT_AGE_16 |
| }.then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_16, tvName.text) |
| assertEquals(PRESIDENT_AGE_16.toString(), tvAge.text) |
| } |
| } |
| |
| @Test // b/120836313 |
| fun testCGModelView_DataModel(): Unit = ensureSetup { |
| val tvNameId = 384 |
| val tvAgeId = 385 |
| |
| var name = PRESIDENT_NAME_1 |
| var age = PRESIDENT_AGE_1 |
| compose( |
| """ |
| @Model |
| data class PersonB(var name: String, var age: Int) |
| |
| @Composable |
| fun PersonViewB(person: PersonB) { |
| Observe { |
| TextView(text=person.name, id=$tvNameId) |
| TextView(text=person.age.toString(), id=$tvAgeId) |
| } |
| } |
| |
| val president = PersonB("$PRESIDENT_NAME_1", $PRESIDENT_AGE_1) |
| """, { mapOf("name" to name, "age" to age) }, """ |
| president.name = name |
| president.age = age |
| """, """ |
| PersonViewB(person=president) |
| """).then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_1, tvName.text) |
| assertEquals(PRESIDENT_AGE_1.toString(), tvAge.text) |
| |
| name = PRESIDENT_NAME_16 |
| age = PRESIDENT_AGE_16 |
| }.then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_16, tvName.text) |
| assertEquals(PRESIDENT_AGE_16.toString(), tvAge.text) |
| } |
| } |
| |
| @Test // b/120843442 |
| fun testCGModelView_ZeroFrame(): Unit = ensureSetup { |
| val tvNameId = 384 |
| val tvAgeId = 385 |
| |
| var name = PRESIDENT_NAME_1 |
| var age = PRESIDENT_AGE_1 |
| compose( |
| """ |
| @Model |
| class PersonC(var name: String, var age: Int) |
| |
| @Composable |
| fun PersonViewC(person: PersonC) { |
| Observe { |
| TextView(text=person.name, id=$tvNameId) |
| TextView(text=person.age.toString(), id=$tvAgeId) |
| } |
| } |
| |
| val president = FrameManager.unframed { PersonC("$PRESIDENT_NAME_1", $PRESIDENT_AGE_1) } |
| """, { mapOf("name" to name, "age" to age) }, """ |
| president.name = name |
| president.age = age |
| """, """ |
| PersonViewC(person=president) |
| """).then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(name, tvName.text) |
| assertEquals(age.toString(), tvAge.text) |
| |
| name = PRESIDENT_NAME_16 |
| age = PRESIDENT_AGE_16 |
| }.then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_16, tvName.text) |
| assertEquals(PRESIDENT_AGE_16.toString(), tvAge.text) |
| } |
| } |
| |
| @Test // b/120843442 |
| fun testCGModelView_ZeroFrame_Modification(): Unit = ensureSetup { |
| val tvNameId = 384 |
| val tvAgeId = 385 |
| |
| var name = PRESIDENT_NAME_1 |
| var age = PRESIDENT_AGE_1 |
| compose( |
| """ |
| @Model |
| class PersonD(var name: String, var age: Int) |
| |
| @Composable |
| fun PersonViewD(person: PersonD) { |
| Observe { |
| TextView(text=person.name, id=$tvNameId) |
| TextView(text=person.age.toString(), id=$tvAgeId) |
| } |
| } |
| |
| val president = FrameManager.framed { PersonD("$PRESIDENT_NAME_1", $PRESIDENT_AGE_1).apply { age = $PRESIDENT_AGE_1 } } |
| """, { mapOf("name" to name, "age" to age) }, """ |
| president.name = name |
| president.age = age |
| """, """ |
| PersonViewD(person=president) |
| """).then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_1, tvName.text) |
| assertEquals(PRESIDENT_AGE_1.toString(), tvAge.text) |
| |
| name = PRESIDENT_NAME_16 |
| age = PRESIDENT_AGE_16 |
| }.then { activity -> |
| val tvName = activity.findViewById(tvNameId) as TextView |
| val tvAge = activity.findViewById(tvAgeId) as TextView |
| assertEquals(PRESIDENT_NAME_16, tvName.text) |
| assertEquals(PRESIDENT_AGE_16.toString(), tvAge.text) |
| } |
| } |
| |
| fun compose( |
| prefix: String, |
| valuesFactory: () -> Map<String, Any>, |
| advance: String, |
| composition: String, |
| dumpClasses: Boolean = false |
| ): RobolectricComposeTester { |
| val className = "Test_${uniqueNumber++}" |
| val fileName = "$className.kt" |
| |
| val candidateValues = valuesFactory() |
| |
| @Suppress("NO_REFLECTION_IN_CLASS_PATH") |
| val parameterList = candidateValues.map { |
| "${it.key}: ${it.value::class.qualifiedName}" |
| }.joinToString() |
| val parameterTypes = candidateValues.map { |
| it.value::class.javaPrimitiveType ?: it.value::class.javaObjectType |
| }.toTypedArray() |
| |
| val compiledClasses = classLoader(""" |
| import android.content.Context |
| import android.widget.* |
| import androidx.compose.* |
| |
| $prefix |
| |
| class $className { |
| |
| @Composable |
| fun compose() { |
| $composition |
| } |
| |
| fun advance($parameterList) { |
| $advance |
| } |
| } |
| """, fileName, dumpClasses) |
| |
| val allClassFiles = compiledClasses.allGeneratedFiles.filter { |
| it.relativePath.endsWith(".class") |
| } |
| |
| val instanceClass = run { |
| var instanceClass: Class<*>? = null |
| var loadedOne = false |
| for (outFile in allClassFiles) { |
| val bytes = outFile.asByteArray() |
| val loadedClass = loadClass( |
| this.javaClass.classLoader!!, |
| null, |
| bytes |
| ) |
| if (loadedClass.name == className) instanceClass = loadedClass |
| loadedOne = true |
| } |
| if (!loadedOne) error("No classes loaded") |
| instanceClass ?: error("Could not find class $className in loaded classes") |
| } |
| |
| val instanceOfClass = instanceClass.newInstance() |
| val advanceMethod = instanceClass.getMethod("advance", *parameterTypes) |
| val composeMethod = if (ComposeFlags.COMPOSER_PARAM) |
| instanceClass.getMethod("compose", Composer::class.java) |
| else |
| instanceClass.getMethod("compose") |
| |
| return composeMulti({ |
| if (ComposeFlags.COMPOSER_PARAM) { |
| composeMethod.invoke(instanceOfClass, currentComposerNonNull) |
| } else { |
| composeMethod.invoke(instanceOfClass) |
| } |
| }) { |
| val values = valuesFactory() |
| val arguments = values.map { it.value }.toTypedArray() |
| advanceMethod.invoke(instanceOfClass, *arguments) |
| } |
| } |
| } |