diff --git a/web/compose-compiler-integration/build.gradle.kts b/web/compose-compiler-integration/build.gradle.kts index 03de875e9e..4ce8479171 100644 --- a/web/compose-compiler-integration/build.gradle.kts +++ b/web/compose-compiler-integration/build.gradle.kts @@ -1,3 +1,41 @@ +plugins { + kotlin("multiplatform") + id("org.jetbrains.compose") +} + + +kotlin { + js(IR) { + browser() { + testTask { + testLogging.showStandardStreams = true + useKarma { + useChromeHeadless() + useFirefox() + } + } + } + binaries.executable() + } + + sourceSets { + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + implementation(compose.runtime) + implementation(project(":web-core")) + } + } + + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} + + fun cloneTemplate(templateName: String, contentMain: String, contentLib: String): File { val tempDir = file("${project.buildDir.absolutePath}/temp/cloned-$templateName") tempDir.deleteRecursively() diff --git a/web/compose-compiler-integration/src/jsMain/kotlin/CrossmoduleTestsDependencies.kt b/web/compose-compiler-integration/src/jsMain/kotlin/CrossmoduleTestsDependencies.kt new file mode 100644 index 0000000000..f0885903a5 --- /dev/null +++ b/web/compose-compiler-integration/src/jsMain/kotlin/CrossmoduleTestsDependencies.kt @@ -0,0 +1,88 @@ +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import kotlinx.browser.document + +data class DataClassTakesValComposable(val c: @Composable () -> Unit) +data class DataClassTakesValComposableTyped(val c: @Composable (T) -> Unit) +data class DataClassTakesVarComposable(var c: @Composable () -> Unit) + +class ClassTakesValComposable(val c: @Composable () -> Unit) +class ClassTakesValComposableTyped(val c: @Composable (T) -> Unit) +class ClassTakesVarComposable(var c: @Composable () -> Unit) + +class ClassTakesComposablePrivateVal(private val c: @Composable () -> Unit) { + + @Composable + fun callPrivateComposablePassedIntoConstructor() { + c() + } +} + + +interface HasComposable { + val composable: @Composable () -> Unit +} + +class ImplementsHasComposable(override val composable: @Composable () -> Unit): HasComposable + +interface HasComposableTyped { + val composable: @Composable (T) -> Unit +} + +class ImplementsHasComposableTyped(override val composable: @Composable (T) -> Unit): HasComposableTyped + +class ClassSavesComposableIntoVar(c: @Composable () -> Unit) { + var composableVar: @Composable () -> Unit = c +} + +class ClassSavesComposableIntoLateinitVar(c: @Composable () -> Unit) { + lateinit var composableVar: @Composable () -> Unit + + init { + composableVar = c + } +} + +class ClassSavesComposableIntoNullableVar(c: @Composable () -> Unit) { + var composableVar: (@Composable () -> Unit)? = null + + init { + composableVar = c + } +} + +class ClassSavesTypedComposableIntoVar(c: @Composable (T) -> Unit) { + var composableVar: @Composable (T) -> Unit = c +} + + +class ClassSavesTypedComposableIntoLateinitVar(c: @Composable (T) -> Unit) { + lateinit var composableVar: @Composable (T) -> Unit + + init { + composableVar = c + } +} + +class ClassWithSecondaryConstructorSavesComposable(val c: @Composable () -> Unit) { + constructor(): this({ + SideEffect { + document.body!!.innerHTML = "Secondary constructor composable content" + } + }) +} + +data class DataClassTakesValStringAndComposable( + val s: String, val c: @Composable DataClassTakesValStringAndComposable.() -> Unit +) + +class ClassTakesValStringAndComposable( + val s: String, val c: @Composable ClassTakesValStringAndComposable.() -> Unit +) + +class ClassSavesStringAndComposableIntoVar( + s: String, c: @Composable ClassSavesStringAndComposableIntoVar.() -> Unit +) { + var composableVar: @Composable ClassSavesStringAndComposableIntoVar.() -> Unit = c + var stringVar: String = s +} diff --git a/web/compose-compiler-integration/src/jsTest/kotlin/ComposablesInConstructorTests.kt b/web/compose-compiler-integration/src/jsTest/kotlin/ComposablesInConstructorTests.kt new file mode 100644 index 0000000000..a9759bb77c --- /dev/null +++ b/web/compose-compiler-integration/src/jsTest/kotlin/ComposablesInConstructorTests.kt @@ -0,0 +1,283 @@ +import androidx.compose.runtime.SideEffect +import kotlinx.browser.document +import org.jetbrains.compose.web.renderComposableInBody +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Classes under tests are defined in jsMain intentionally. + * The reason is to test cross-module compilation + runtime behaviour. + * + * Reporeted Issues: + * https://github.com/JetBrains/compose-jb/issues/746 + * https://github.com/JetBrains/compose-jb/issues/1052 + */ +class ComposablesInConstructorTests { + + @Test + fun valComposableInDataClass() { + val d = DataClassTakesValComposable { + SideEffect { + document.body!!.innerText = "DataClassTakesValComposable" + } + } + + renderComposableInBody { + d.c() + } + + assertEquals("DataClassTakesValComposable", document.body!!.innerText) + } + + @Test + fun valTypedComposableInDataClass() { + val d = DataClassTakesValComposableTyped { + SideEffect { + document.body!!.innerText = "DataClassTakesValComposableTyped-$it" + } + } + + renderComposableInBody { + d.c("WORKS") + } + + assertEquals("DataClassTakesValComposableTyped-WORKS", document.body!!.innerText) + } + + @Test + fun varComposableInDataClass() { + val d = DataClassTakesVarComposable { + SideEffect { + document.body!!.innerText = "DataClassTakesVarComposable" + } + } + + renderComposableInBody { + d.c() + } + + assertEquals("DataClassTakesVarComposable", document.body!!.innerText) + } + + @Test + fun valComposableInClass() { + val d = ClassTakesValComposable { + SideEffect { + document.body!!.innerText = "ClassTakesValComposable" + } + } + + renderComposableInBody { + d.c() + } + + assertEquals("ClassTakesValComposable", document.body!!.innerText) + } + + @Test + fun valTypedComposableInClass() { + val d = ClassTakesValComposableTyped { + SideEffect { + document.body!!.innerText = "ClassTakesValComposableTyped-$it" + } + } + + renderComposableInBody { + d.c(100500) + } + + assertEquals("ClassTakesValComposableTyped-100500", document.body!!.innerText) + } + + @Test + fun varComposableInClass() { + val d = ClassTakesVarComposable { + SideEffect { + document.body!!.innerText = "ClassTakesVarComposable" + } + } + + renderComposableInBody { + d.c() + } + + assertEquals("ClassTakesVarComposable", document.body!!.innerText) + } + + @Test + fun implementsHasComposable() { + val d: HasComposable = ImplementsHasComposable { + SideEffect { + document.body!!.innerText = "ImplementsHasComposable" + } + } + + renderComposableInBody { + d.composable() + } + + assertEquals("ImplementsHasComposable", document.body!!.innerText) + } + + @Test + fun implementsHasComposableTyped() { + val d: HasComposableTyped = ImplementsHasComposableTyped { + SideEffect { + document.body!!.innerText = "ImplementsHasComposableTyped-$it" + } + } + + renderComposableInBody { + d.composable(123456) + } + + assertEquals("ImplementsHasComposableTyped-123456", document.body!!.innerText) + } + + @Test + fun classSavesComposableIntoVar() { + val d = ClassSavesComposableIntoVar { + SideEffect { + document.body!!.innerText = "ClassSavesComposableIntoVar" + } + } + + renderComposableInBody { + d.composableVar() + } + + assertEquals("ClassSavesComposableIntoVar", document.body!!.innerText) + } + + @Test + fun classSavesComposableIntoLateinitVar() { + val d = ClassSavesComposableIntoLateinitVar { + SideEffect { + document.body!!.innerText = "ClassSavesComposableIntoLateinitVar" + } + } + + renderComposableInBody { + d.composableVar() + } + + assertEquals("ClassSavesComposableIntoLateinitVar", document.body!!.innerText) + } + + @Test + fun classSavesComposableIntoNullableVar() { + val d = ClassSavesComposableIntoNullableVar { + SideEffect { + document.body!!.innerText = "ClassSavesComposableIntoNullableVar" + } + } + + renderComposableInBody { + d.composableVar!!.invoke() + } + + assertEquals("ClassSavesComposableIntoNullableVar", document.body!!.innerText) + } + + + @Test + fun classSavesTypedComposableIntoVar() { + val d = ClassSavesTypedComposableIntoVar { + SideEffect { + document.body!!.innerText = "ClassSavesTypedComposableIntoVar-$it" + } + } + + renderComposableInBody { + d.composableVar("ABC") + } + + assertEquals("ClassSavesTypedComposableIntoVar-ABC", document.body!!.innerText) + } + + @Test + fun classSavesTypedComposableIntoLateinitVar() { + val d = ClassSavesTypedComposableIntoLateinitVar { + SideEffect { + document.body!!.innerText = "ClassSavesTypedComposableIntoLateinitVar-$it" + } + } + + renderComposableInBody { + d.composableVar("ABC") + } + + assertEquals("ClassSavesTypedComposableIntoLateinitVar-ABC", document.body!!.innerText) + } + + @Test + fun classWithSecondaryConstructorSavesComposable() { + val d = ClassWithSecondaryConstructorSavesComposable() + + renderComposableInBody { + d.c() + } + + assertEquals("Secondary constructor composable content", document.body!!.innerText) + } + + @Test + fun dataClassTakesValStringAndComposable() { + val d = DataClassTakesValStringAndComposable("String1") { + SideEffect { + document.body!!.innerText = "DataClassTakesValStringAndComposable-${this.s}" + } + } + + renderComposableInBody { + d.c(d) + } + + assertEquals("DataClassTakesValStringAndComposable-String1", document.body!!.innerText) + } + + @Test + fun classTakesValStringAndComposable() { + val d = ClassTakesValStringAndComposable("123123") { + SideEffect { + document.body!!.innerText = "ClassTakesValStringAndComposable-${this.s}" + } + } + + renderComposableInBody { + d.c(d) + } + + assertEquals("ClassTakesValStringAndComposable-123123", document.body!!.innerText) + } + + @Test + fun classSavesStringAndComposableIntoVar() { + val d = ClassSavesStringAndComposableIntoVar("098765") { + SideEffect { + document.body!!.innerText = "ClassSavesStringAndComposableIntoVar-${this.stringVar}" + } + } + + renderComposableInBody { + d.composableVar(d) + } + + assertEquals("ClassSavesStringAndComposableIntoVar-098765", document.body!!.innerText) + } + + @Test + fun classTakesComposablePrivateVal() { + val d = ClassTakesComposablePrivateVal { + SideEffect { + document.body!!.innerText = "ClassTakesComposablePrivateVal" + } + } + + renderComposableInBody { + d.callPrivateComposablePassedIntoConstructor() + } + + assertEquals("ClassTakesComposablePrivateVal", document.body!!.innerText) + } +} diff --git a/web/core/src/jsTest/kotlin/FailingTestCases.kt b/web/core/src/jsTest/kotlin/FailingTestCases.kt index 3cdc0336ab..a23fc3b022 100644 --- a/web/core/src/jsTest/kotlin/FailingTestCases.kt +++ b/web/core/src/jsTest/kotlin/FailingTestCases.kt @@ -24,19 +24,6 @@ class FailingTestCases { } assertTrue(expectedErrorThrown) } - - data class DataClassTakesComposable(val c: @Composable () -> Unit) - class UsualClassTakesComposable(val c: @Composable () -> Unit) - - @Test - fun passingComposableIntoConstructorOfDataClass() { - check(DataClassTakesComposable {}.c == null) // Expected behaviour: c != null - } - - @Test - fun passingComposableIntoConstructorOfUsualClass() { - check(UsualClassTakesComposable {}.c == null) // Expected behaviour: c != null - } } @Suppress("Unused", "NOTHING_TO_INLINE", "NESTED_CLASS_IN_EXTERNAL_INTERFACE", "INLINE_EXTERNAL_DECLARATION", "WRONG_BODY_OF_EXTERNAL_DECLARATION", "NESTED_EXTERNAL_DECLARATION", "ClassName")