diff --git a/.gitignore b/.gitignore index cbe75da2..655897ec 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ .cxx local.properties /.idea/ +.kotlin \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore index 26d33521..f94a505c 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,3 +1,6 @@ # Default ignored files /shelf/ /workspace.xml +compiler.xml +gradle.xml +misc.xml \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 6bff30cc..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 1f02d34a..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 10b5b506..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/convention.android-base.gradle.kts b/buildSrc/src/main/kotlin/convention.android-base.gradle.kts index 1bfc3d5c..48d5650e 100644 --- a/buildSrc/src/main/kotlin/convention.android-base.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.android-base.gradle.kts @@ -1,12 +1,27 @@ -import com.android.build.gradle.BaseExtension +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension import io.github.kakaocup.withVersionCatalog withVersionCatalog { libs -> - configure { - compileSdkVersion(libs.versions.compileSdk.get().toInt()) - defaultConfig { - minSdk = libs.versions.minSdk.get().toInt() - multiDexEnabled = true + val compileSdkInt = libs.versions.compileSdk.get().toInt() + val minSdkInt = libs.versions.minSdk.get().toInt() + + pluginManager.withPlugin("com.android.library") { + extensions.configure("android") { + compileSdk = compileSdkInt + defaultConfig { + minSdk = minSdkInt + multiDexEnabled = true + } + } + } + pluginManager.withPlugin("com.android.application") { + extensions.configure("android") { + compileSdk = compileSdkInt + defaultConfig { + minSdk = minSdkInt + multiDexEnabled = true + } } } } diff --git a/buildSrc/src/main/kotlin/convention.application.gradle.kts b/buildSrc/src/main/kotlin/convention.application.gradle.kts index 6751c331..23f63b11 100644 --- a/buildSrc/src/main/kotlin/convention.application.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.application.gradle.kts @@ -1,4 +1,3 @@ -import com.android.build.gradle.AppExtension import io.github.kakaocup.withVersionCatalog plugins { @@ -8,7 +7,7 @@ plugins { } withVersionCatalog { libs -> - configure() { + android { defaultConfig { targetSdk = libs.versions.targetSdk.get().toInt() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/buildSrc/src/main/kotlin/convention.kotlin.gradle.kts b/buildSrc/src/main/kotlin/convention.kotlin.gradle.kts index 135b5016..13fb1d5b 100644 --- a/buildSrc/src/main/kotlin/convention.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.kotlin.gradle.kts @@ -1,11 +1,8 @@ import io.github.kakaocup.withVersionCatalog - -plugins { - kotlin("android") -} +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension withVersionCatalog { libs -> - kotlin { + extensions.getByType(KotlinAndroidProjectExtension::class.java).apply { jvmToolchain(libs.versions.jvmVersion.get().toInt()) } } diff --git a/buildSrc/src/main/kotlin/convention.library.gradle.kts b/buildSrc/src/main/kotlin/convention.library.gradle.kts index 61134b62..66f58bf6 100644 --- a/buildSrc/src/main/kotlin/convention.library.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.library.gradle.kts @@ -3,7 +3,3 @@ plugins { id("convention.android-base") id("convention.kotlin") } - -kotlin { - jvmToolchain(8) -} diff --git a/buildSrc/src/main/kotlin/convention.publishing.gradle.kts b/buildSrc/src/main/kotlin/convention.publishing.gradle.kts index 2f3bad6a..64184dad 100644 --- a/buildSrc/src/main/kotlin/convention.publishing.gradle.kts +++ b/buildSrc/src/main/kotlin/convention.publishing.gradle.kts @@ -1,4 +1,4 @@ -import com.android.build.gradle.LibraryExtension +import com.android.build.api.dsl.LibraryExtension import io.github.kakaocup.Github import java.net.URI diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index c9863d73..9aff9ad7 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -1,15 +1,33 @@ plugins { - id("convention.library") - id("convention.publishing") + id("org.jetbrains.kotlin.multiplatform") + id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.jetbrains.compose) + id("org.jetbrains.kotlin.plugin.compose") } -android { - namespace = "io.github.kakaocup.compose" -} +kotlin { + jvmToolchain(libs.versions.jvmVersion.get().toInt()) + + android { + namespace = "io.github.kakaocup.compose" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } -dependencies { - implementation(libs.androidx.test.espresso.espressoCore) - implementation(libs.androidx.test.ext.junit) + iosX64() + iosArm64() + iosSimulatorArm64() - implementation(libs.androidx.compose.ui.uiTestJunit4) + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.ui) + implementation(libs.jetbrains.compose.ui.test) + } + androidMain.dependencies { + implementation(libs.androidx.compose.ui.uiTestJunit4) + implementation(libs.androidx.test.ext.junit) + } + } } diff --git a/compose/src/main/AndroidManifest.xml b/compose/src/androidMain/AndroidManifest.xml similarity index 100% rename from compose/src/main/AndroidManifest.xml rename to compose/src/androidMain/AndroidManifest.xml diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/LazyListNodeAssertions.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/LazyListNodeAssertions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/LazyListNodeAssertions.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/LazyListNodeAssertions.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/ListNodeAssertions.kt diff --git a/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt new file mode 100644 index 00000000..8566db1f --- /dev/null +++ b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt @@ -0,0 +1,46 @@ +package io.github.kakaocup.compose.node.assertion + +import androidx.annotation.StringRes +import androidx.compose.ui.semantics.SemanticsProperties +import androidx.compose.ui.test.* +import io.github.kakaocup.compose.utilities.getResourceString + +actual interface TextResourcesNodeAssertions : NodeAssertions { + fun assertContentDescriptionEquals( + @StringRes vararg values: Int + ) { + assertContentDescriptionEquals(values = values.map(::getResourceString).toTypedArray()) + } + + fun assertContentDescriptionContains( + @StringRes value: Int, + substring: Boolean = false, + ignoreCase: Boolean = false + ) { + assertContentDescriptionContains(getResourceString(value), substring, ignoreCase) + } + + fun assertTextEquals( + @StringRes vararg values: Int, + includeEditableText: Boolean = true + ) { + assertTextEquals( + values = values.map(::getResourceString).toTypedArray(), + includeEditableText = includeEditableText + ) + } + + fun assertTextContains( + @StringRes value: Int, + substring: Boolean = false, + ignoreCase: Boolean = false + ) { + assertTextContains(getResourceString(value), substring, ignoreCase) + } + + fun assertValueEquals(@StringRes value: Int) { + delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { + assertValueEquals(getResourceString(value)) + } + } +} diff --git a/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeAndroid.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeAndroid.kt new file mode 100644 index 00000000..a317adb4 --- /dev/null +++ b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeAndroid.kt @@ -0,0 +1,38 @@ +package io.github.kakaocup.compose.node.core + +import androidx.compose.ui.test.SemanticsNodeInteraction +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import androidx.compose.ui.test.junit4.ComposeTestRule +import io.github.kakaocup.compose.node.builder.NodeMatcher + +@PublishedApi +internal actual inline fun createChildNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>, +): N = N::class.java + .getConstructor( + SemanticsNodeInteractionsProvider::class.java, + NodeMatcher::class.java, + BaseNode::class.java, + ) + .newInstance(semanticsProvider, nodeMatcher, parentNode) + +/** + * Android-only `waitUntil` helper. Depends on JUnit4-flavoured [ComposeTestRule], + * which only exists on Android. iOS tests should use `ComposeUiTest.waitUntil` directly. + */ +fun BaseNode<*>.waitUntil( + composeTestRule: ComposeTestRule = semanticsProvider as ComposeTestRule, + timeoutMillis: Long = 1_000, + condition: SemanticsNodeInteraction.() -> Unit +) { + composeTestRule.waitUntil(timeoutMillis) { + try { + condition.invoke(this.delegate.interaction.semanticsNodeInteraction) + true + } catch (e: AssertionError) { + false + } + } +} diff --git a/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenAndroid.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenAndroid.kt new file mode 100644 index 00000000..c9d8d296 --- /dev/null +++ b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenAndroid.kt @@ -0,0 +1,14 @@ +package io.github.kakaocup.compose.node.element + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider + +@PublishedApi +internal actual inline fun > instantiateScreen( + semanticsProvider: SemanticsNodeInteractionsProvider, +): T = T::class.java + .getDeclaredConstructor(SemanticsNodeInteractionsProvider::class.java) + .newInstance(semanticsProvider) + +@PublishedApi +internal actual inline fun > instantiateScreen(): T = + T::class.java.getDeclaredConstructor().newInstance() diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemBuilder.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemBuilder.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemBuilder.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemBuilder.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListItemNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/KLazyListNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/LazyListItemProvidedException.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/LazyListItemProvidedException.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/lazylist/LazyListItemProvidedException.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/lazylist/LazyListItemProvidedException.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/list/KListItemNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/node/element/list/KListNode.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt rename to compose/src/androidMain/kotlin/io/github/kakaocup/compose/rule/KakaoComposeTestRule.kt diff --git a/compose/src/androidMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt new file mode 100644 index 00000000..4758acbc --- /dev/null +++ b/compose/src/androidMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt @@ -0,0 +1,7 @@ +package io.github.kakaocup.compose.utilities + +import androidx.annotation.StringRes +import androidx.test.platform.app.InstrumentationRegistry + +actual fun getResourceString(@StringRes resId: Int): String = + InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resId) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/KakaoCompose.kt similarity index 97% rename from compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/KakaoCompose.kt index 75bcaf88..7000fba6 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/KakaoCompose.kt +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/KakaoCompose.kt @@ -2,7 +2,6 @@ package io.github.kakaocup.compose import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import io.github.kakaocup.compose.intercept.base.Interceptor -import io.github.kakaocup.compose.rule.KakaoComposeTestRule import io.github.kakaocup.compose.intercept.interaction.ComposeInteraction import io.github.kakaocup.compose.intercept.operation.ComposeAction import io.github.kakaocup.compose.intercept.operation.ComposeAssertion diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/exception/KakaoComposeException.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/base/Interception.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/base/Interception.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/base/Interception.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/base/Interception.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt similarity index 97% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt index e0629d69..64d263c9 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/base/Interceptor.kt @@ -1,6 +1,5 @@ package io.github.kakaocup.compose.intercept.base -import androidx.test.espresso.ViewInteraction import io.github.kakaocup.compose.intercept.base.Interceptor.Builder import io.github.kakaocup.compose.intercept.interaction.ComposeInteraction import io.github.kakaocup.compose.intercept.operation.ComposeAction @@ -103,9 +102,9 @@ class Interceptor( private var composeInterceptor: Interceptor? = null /** - * Setups the interceptor for `check` and `perform` operations happening through [ViewInteraction] + * Setups the interceptor for `check` and `perform` operations happening through [ComposeInteraction] * - * @param builder Builder of interceptor for [ViewInteraction] + * @param builder Builder of interceptor for [ComposeInteraction] */ fun onComposeInteraction(builder: Builder.() -> Unit) { composeInterceptor = Builder().apply(builder).build() diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeDelegate.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeDelegate.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeDelegate.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeDelegate.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeInterceptable.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeInterceptable.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeInterceptable.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/ComposeInterceptable.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/Delegate.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/Delegate.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/delegate/Delegate.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/delegate/Delegate.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/interaction/ComposeInteraction.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/interaction/ComposeInteraction.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/interaction/ComposeInteraction.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/interaction/ComposeInteraction.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/interaction/Interaction.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/interaction/Interaction.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/interaction/Interaction.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/interaction/Interaction.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperation.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperation.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperation.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperation.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationImpls.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationImpls.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationImpls.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationImpls.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationType.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationType.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationType.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/intercept/operation/ComposeOperationType.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/action/NodeActions.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/action/NodeActions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/action/NodeActions.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/action/NodeActions.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/action/TextActions.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/action/TextActions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/action/TextActions.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/action/TextActions.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/NodeAssertions.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/assertion/NodeAssertions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/NodeAssertions.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/assertion/NodeAssertions.kt diff --git a/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt new file mode 100644 index 00000000..0f72d7d0 --- /dev/null +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt @@ -0,0 +1,10 @@ +package io.github.kakaocup.compose.node.assertion + +/** + * Marker interface for resource-id based assertions. The actual methods are declared + * on the Android-specific `actual interface` (since iOS lacks `@StringRes` resource ids). + * + * On iOS this is an empty marker — instantiate screens directly and call the + * string-based [NodeAssertions] methods instead. + */ +expect interface TextResourcesNodeAssertions : NodeAssertions diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/NodeMatcher.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeProvider.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/NodeProvider.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/NodeProvider.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/NodeProvider.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt similarity index 80% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt index 4987a494..12537395 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/builder/ViewBuilder.kt @@ -1,6 +1,5 @@ package io.github.kakaocup.compose.node.builder -import androidx.annotation.StringRes import androidx.compose.ui.semantics.* import androidx.compose.ui.test.* import androidx.compose.ui.text.input.ImeAction @@ -142,20 +141,11 @@ class ViewBuilder { ) = addFilter(androidx.compose.ui.test.hasContentDescription(value, substring, ignoreCase)) /** - * Returns whether the node's content description contains the given [value]. - * - * Note that in merged semantics tree there can be a list of content descriptions that got merged - * from the child nodes. Typically an accessibility tooling will decide based on its heuristics - * which ones to announce. - * - * @param resId String resource id to match as one of the items in the list of content descriptions. - * @param substring Whether to use substring matching. - * @param ignoreCase Whether case should be ignored. - * - * @see SemanticsProperties.ContentDescription + * Resource-id variant of [hasContentDescription]. Available on Android only at + * runtime; on iOS the lookup throws [UnsupportedOperationException]. */ fun hasContentDescription( - @StringRes resId: Int, + resId: Int, substring: Boolean = false, ignoreCase: Boolean = false ) = hasContentDescription(getResourceString(resId), substring, ignoreCase) @@ -176,20 +166,9 @@ class ViewBuilder { vararg values: String ) = addFilter(androidx.compose.ui.test.hasContentDescriptionExactly(values = values)) - /** - * Returns whether the node's content description contains exactly the given [values] and nothing - * else. - * - * Note that in merged semantics tree there can be a list of content descriptions that got merged - * from the child nodes. Typically an accessibility tooling will decide based on its heuristics - * which ones to announce. - * - * @param resIds List of string resources id's values to match (the order does not matter) - * - * @see SemanticsProperties.ContentDescription - */ + /** Resource-id variant. Android-only at runtime. */ fun hasContentDescriptionExactly( - @StringRes vararg resIds: Int + vararg resIds: Int ) = hasContentDescriptionExactly(values = resIds.map(::getResourceString).toTypedArray()) /** @@ -214,24 +193,9 @@ class ViewBuilder { ignoreCase: Boolean = false ) = addFilter(androidx.compose.ui.test.hasText(text, substring, ignoreCase)) - /** - * Returns whether the node's text contains the given [text]. - * - * This will also search in [SemanticsProperties.EditableText]. - * - * Note that in merged semantics tree there can be a list of text items that got merged from - * the child nodes. Typically an accessibility tooling will decide based on its heuristics which - * ones to use. - * - * @param resId String resource id value to match as one of the items in the list of text values. - * @param substring Whether to use substring matching. - * @param ignoreCase Whether case should be ignored. - * - * @see SemanticsProperties.Text - * @see SemanticsProperties.EditableText - */ + /** Resource-id variant of [hasText]. Android-only at runtime. */ fun hasText( - @StringRes resId: Int, + resId: Int, substring: Boolean = false, ignoreCase: Boolean = false ) = hasText(getResourceString(resId), substring, ignoreCase) @@ -258,23 +222,9 @@ class ViewBuilder { textValues = textValues, includeEditableText = includeEditableText)) - /** - * Returns whether the node's text contains exactly the given [values] and nothing else. - * - * This will also search in [SemanticsProperties.EditableText] by default. - * - * Note that in merged semantics tree there can be a list of text items that got merged from - * the child nodes. Typically an accessibility tooling will decide based on its heuristics which - * ones to use. - * - * @param resIds Values List of string resources id's to match (the order does not matter) - * @param includeEditableText Whether to also assert against the editable text. - * - * @see SemanticsProperties.Text - * @see SemanticsProperties.EditableText - */ + /** Resource-id variant. Android-only at runtime. */ fun hasTextExactly( - @StringRes vararg resIds: Int, + vararg resIds: Int, includeEditableText: Boolean = true ) = hasTextExactly( textValues = resIds.map(::getResourceString).toTypedArray(), @@ -291,14 +241,8 @@ class ViewBuilder { fun hasStateDescription(value: String) = addFilter(androidx.compose.ui.test.hasStateDescription(value)) - /** - * Returns whether the node's value matches exactly to the given accessibility value. - * - * @param resId String resource id value to match. - * - * @see SemanticsProperties.StateDescription - */ - fun hasStateDescription(@StringRes resId: Int) = hasStateDescription(getResourceString(resId)) + /** Resource-id variant. Android-only at runtime. */ + fun hasStateDescription(resId: Int) = hasStateDescription(getResourceString(resId)) /** * Returns whether the node is marked as an accessibility header. @@ -326,14 +270,8 @@ class ViewBuilder { */ fun hasTestTag(testTag: String) = addFilter(androidx.compose.ui.test.hasTestTag(testTag)) - /** - * Returns whether the node is annotated by the given test tag. - * - * @param resId String resource id to match. - * - * @see SemanticsProperties.TestTag - */ - fun hasTestTag(@StringRes resId: Int) = hasTestTag(getResourceString(resId)) + /** Resource-id variant. Android-only at runtime. */ + fun hasTestTag(resId: Int) = hasTestTag(getResourceString(resId)) /** * Returns whether the node is a dialog. diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt similarity index 80% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt index 67824b25..1c5b7313 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/BaseNode.kt @@ -1,12 +1,8 @@ package io.github.kakaocup.compose.node.core import androidx.compose.ui.test.SemanticsMatcher -import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.SemanticsNodeInteractionsProvider -import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.hasAnyAncestor -import androidx.compose.ui.test.isDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule import io.github.kakaocup.compose.intercept.delegate.ComposeDelegate import io.github.kakaocup.compose.intercept.delegate.ComposeInterceptable import io.github.kakaocup.compose.node.action.NodeActions @@ -64,11 +60,7 @@ abstract class BaseNode> constructor( } inline fun child(function: ViewBuilder.() -> Unit): N { - return N::class.java.getConstructor( - SemanticsNodeInteractionsProvider::class.java, - NodeMatcher::class.java, - BaseNode::class.java, - ).newInstance( + return createChildNode( semanticsProvider.orGlobal().checkNotNull(), ViewBuilder().apply(function).build(), this, @@ -104,19 +96,11 @@ abstract class BaseNode> constructor( return semanticsMatcherList.reduce { finalMatcher, matcher -> finalMatcher and matcher } } - - fun waitUntil( - composeTestRule: ComposeTestRule = semanticsProvider as ComposeTestRule, - timeoutMillis: Long = 1_000, - condition: SemanticsNodeInteraction.() -> Unit - ) { - composeTestRule.waitUntil(timeoutMillis) { - try { - condition.invoke(this.delegate.interaction.semanticsNodeInteraction) - true - } catch (e: AssertionError) { - false - } - } - } } + +@PublishedApi +internal expect inline fun createChildNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>, +): N diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/ComposeMarker.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/ComposeMarker.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/core/ComposeMarker.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/ComposeMarker.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/core/KDSL.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/KDSL.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/core/KDSL.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/core/KDSL.kt diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt similarity index 82% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt index c9f7136f..473c73bf 100644 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreen.kt @@ -43,19 +43,18 @@ open class ComposeScreen> : BaseNode { inline fun > onComposeScreen( semanticsProvider: SemanticsNodeInteractionsProvider, noinline function: T.() -> Unit, - ): T = T::class.java - .getDeclaredConstructor( - SemanticsNodeInteractionsProvider::class.java - ) - .newInstance(semanticsProvider) - .apply { this(function) } + ): T = instantiateScreen(semanticsProvider).apply { this(function) } inline fun > onComposeScreen( noinline function: T.() -> Unit, - ): T = - T::class.java.getDeclaredConstructor() - .newInstance() - .apply { this(function) } - + ): T = instantiateScreen().apply { this(function) } } } + +@PublishedApi +internal expect inline fun > instantiateScreen( + semanticsProvider: SemanticsNodeInteractionsProvider, +): T + +@PublishedApi +internal expect inline fun > instantiateScreen(): T diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/element/KNode.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/node/element/KNode.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/node/element/KNode.kt diff --git a/compose/src/commonMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt new file mode 100644 index 00000000..5d963fe6 --- /dev/null +++ b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt @@ -0,0 +1,10 @@ +package io.github.kakaocup.compose.utilities + +/** + * Resolves an Android string resource id to its localized string. + * + * Android: looks up via instrumentation context. + * iOS: throws [UnsupportedOperationException] — iOS has no Android-style resource ids; + * pass string literals (or use Compose Resources) instead. + */ +expect fun getResourceString(resId: Int): String diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt b/compose/src/commonMain/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt similarity index 100% rename from compose/src/main/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt rename to compose/src/commonMain/kotlin/io/github/kakaocup/compose/utilities/Extensions.kt diff --git a/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt new file mode 100644 index 00000000..24ea8951 --- /dev/null +++ b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt @@ -0,0 +1,3 @@ +package io.github.kakaocup.compose.node.assertion + +actual interface TextResourcesNodeAssertions : NodeAssertions diff --git a/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeIos.kt b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeIos.kt new file mode 100644 index 00000000..65e67332 --- /dev/null +++ b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/core/BaseNodeIos.kt @@ -0,0 +1,28 @@ +package io.github.kakaocup.compose.node.core + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.builder.NodeMatcher +import io.github.kakaocup.compose.node.element.KNode + +/** + * iOS lacks the reflection used on Android to construct arbitrary [BaseNode] subtypes. + * For the common case where consumers want a plain [KNode] (i.e. `val x: KNode = child { … }`) + * we instantiate a [KNode] and rely on the reified cast at the call site. + * + * If a caller picks a non-[KNode] subtype on iOS, the reified cast throws + * [ClassCastException] with a clear message — declare your screens' children as + * [KNode] (or construct the custom subclass directly) on iOS. + */ +@PublishedApi +internal actual inline fun createChildNode( + semanticsProvider: SemanticsNodeInteractionsProvider, + nodeMatcher: NodeMatcher, + parentNode: BaseNode<*>, +): N { + val node = KNode( + semanticsProvider = semanticsProvider, + nodeMatcher = nodeMatcher, + parentNode = parentNode, + ) + return node as N +} diff --git a/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenIos.kt b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenIos.kt new file mode 100644 index 00000000..1fc0b8d8 --- /dev/null +++ b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/node/element/ComposeScreenIos.kt @@ -0,0 +1,18 @@ +package io.github.kakaocup.compose.node.element + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider + +@PublishedApi +internal actual inline fun > instantiateScreen( + semanticsProvider: SemanticsNodeInteractionsProvider, +): T = throw UnsupportedOperationException( + "Reflection-based onComposeScreen is not supported on iOS. " + + "Instantiate the screen directly: MyScreen(provider).apply { /* … */ }." +) + +@PublishedApi +internal actual inline fun > instantiateScreen(): T = + throw UnsupportedOperationException( + "Reflection-based onComposeScreen is not supported on iOS. " + + "Instantiate the screen directly: MyScreen().apply { /* … */ }." + ) diff --git a/compose/src/iosMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt new file mode 100644 index 00000000..4903c993 --- /dev/null +++ b/compose/src/iosMain/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt @@ -0,0 +1,7 @@ +package io.github.kakaocup.compose.utilities + +actual fun getResourceString(resId: Int): String = + throw UnsupportedOperationException( + "Android string resource ids are not available on iOS. Use string literals " + + "or wire up Compose Resources for cross-platform text." + ) diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt deleted file mode 100644 index 7fb87a68..00000000 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/node/assertion/TextResourcesNodeAssertions.kt +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.kakaocup.compose.node.assertion - -import androidx.annotation.StringRes -import androidx.compose.ui.semantics.SemanticsProperties -import androidx.compose.ui.test.* -import io.github.kakaocup.compose.utilities.getResourceString - -interface TextResourcesNodeAssertions : NodeAssertions { - /** - * Asserts that the node's content description contains exactly the given [values] and nothing else. - * - * Note that in merged semantics tree there can be a list of content descriptions that got merged - * from the child nodes. Typically an accessibility tooling will decide based on its heuristics - * which ones to announce. - * - * Throws [AssertionError] if the node's descriptions don't contain all items from [values], or - * if the descriptions contain extra items that are not in [values]. - * - * @param values List of string resources id's values to match (the order does not matter) - * @see SemanticsProperties.ContentDescription - */ - fun assertContentDescriptionEquals( - @StringRes vararg values: Int - ) { - assertContentDescriptionEquals(values = values.map(::getResourceString).toTypedArray()) - } - - /** - * Asserts that the node's content description contains the given [value]. - * - * Note that in merged semantics tree there can be a list of content descriptions that got merged - * from the child nodes. Typically an accessibility tooling will decide based on its heuristics - * which ones to announce. - * - * Throws [AssertionError] if the node's value does not contain `value`, or if the node has no value - * - * @param value String resource id value to match as one of the items in the list of content descriptions. - * @param substring Whether this can be satisfied as a substring match of an item in the list of - * descriptions. - * @param ignoreCase Whether case should be ignored. - * @see SemanticsProperties.ContentDescription - */ - fun assertContentDescriptionContains( - @StringRes value: Int, - substring: Boolean = false, - ignoreCase: Boolean = false - ) { - assertContentDescriptionContains(getResourceString(value), substring, ignoreCase) - } - - /** - * Asserts that the node's text contains exactly the given [values] and nothing else. - * - * This will also search in [SemanticsProperties.EditableText] by default. - * - * Note that in merged semantics tree there can be a list of text items that got merged from - * the child nodes. Typically an accessibility tooling will decide based on its heuristics which - * ones to use. - * - * Throws [AssertionError] if the node's text values don't contain all items from [values], or - * if the text values contain extra items that are not in [values]. - * - * @param values List of string resources id's to match (the order does not matter) - * @param includeEditableText Whether to also assert against the editable text. - * @see SemanticsProperties.ContentDescription - */ - fun assertTextEquals( - @StringRes vararg values: Int, - includeEditableText: Boolean = true - ) { - assertTextEquals( - values = values.map(::getResourceString).toTypedArray(), - includeEditableText = includeEditableText - ) - } - - /** - * Asserts that the node's text contains the given [value]. - * - * This will also search in [SemanticsProperties.EditableText]. - * - * Note that in merged semantics tree there can be a list of text items that got merged from - * the child nodes. Typically an accessibility tooling will decide based on its heuristics which - * ones to use. - * - * Throws [AssertionError] if the node's value does not contain `value`, or if the node has no value - * - * @param value String resource id value to match as one of the items in the list of text values. - * @param substring Whether this can be satisfied as a substring match of an item in the list of - * text. - * @param ignoreCase Whether case should be ignored. - * @see SemanticsProperties.Text - */ - fun assertTextContains( - @StringRes value: Int, - substring: Boolean = false, - ignoreCase: Boolean = false - ) { - assertTextContains(getResourceString(value), substring, ignoreCase) - } - - /** - * Asserts the node's value equals the given value. - * - * For further details please check [SemanticsProperties.StateDescription]. - * Throws [AssertionError] if the node's value is not equal to `value`, or if the node has no value - */ - fun assertValueEquals(@StringRes value: Int) { - delegate.check(NodeAssertions.ComposeBaseAssertionType.ASSERT_VALUE_EQUALS) { assertValueEquals(getResourceString(value)) } - } -} diff --git a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt b/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt deleted file mode 100644 index 0be1a3d8..00000000 --- a/compose/src/main/kotlin/io/github/kakaocup/compose/utilities/ContextUtils.kt +++ /dev/null @@ -1,6 +0,0 @@ -package io.github.kakaocup.compose.utilities - -import androidx.annotation.StringRes -import androidx.test.platform.app.InstrumentationRegistry - -fun getResourceString(@StringRes resId: Int) = InstrumentationRegistry.getInstrumentation().targetContext.resources.getString(resId) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4af8b289..1e94cd3e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] dokkaGradlePluginVersion = "2.1.0" githubApiVersion = "1.318" -gradle = "8.13.1" -kotlin = "2.0.0" -gradleVersion = "8.13.1" +gradle = "9.2.0" +kotlin = "2.3.20" +gradleVersion = "9.2.0" compileSdk = "36" targetSdk = "36" minSdk = "21" @@ -15,7 +15,7 @@ espressoVersion = "3.7.0" junitVersion = "4.13.2" junitExtVersion = "1.3.0" -kotlinGradlePluginVersion = "2.0.0" +kotlinGradlePluginVersion = "2.3.20" multidexVersion = "2.0.1" materialVersion = "1.13.0" activityComposeVersion = "1.12.1" @@ -23,6 +23,7 @@ composeMaterialVersion = "1.3.1" composeCompilerVersion = "1.5.15" composeVersion = "1.10.0" +composeMultiplatform = "1.10.0" materialsIcon = "1.7.8" [libraries] @@ -52,7 +53,15 @@ androidx-multidex-multidex = { group = "androidx.multidex", name = "multidex", v kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePluginVersion" } org-jetbrains-dokka-kotlinAsJavaPlugin = { group = "org.jetbrains.dokka", name = "kotlin-as-java-plugin", version.ref = "dokkaVersion" } +jetbrains-compose-runtime = { group = "org.jetbrains.compose.runtime", name = "runtime", version.ref = "composeMultiplatform" } +jetbrains-compose-foundation = { group = "org.jetbrains.compose.foundation", name = "foundation", version.ref = "composeMultiplatform" } +jetbrains-compose-material = { group = "org.jetbrains.compose.material", name = "material", version.ref = "composeMultiplatform" } +jetbrains-compose-ui = { group = "org.jetbrains.compose.ui", name = "ui", version.ref = "composeMultiplatform" } +jetbrains-compose-ui-test = { group = "org.jetbrains.compose.ui", name = "ui-test", version.ref = "composeMultiplatform" } + [plugins] org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 24973339..25e9a675 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Oct 16 09:29:44 AEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample-kmp/build.gradle.kts b/sample-kmp/build.gradle.kts new file mode 100644 index 00000000..07382378 --- /dev/null +++ b/sample-kmp/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.jetbrains.compose) + id("org.jetbrains.kotlin.plugin.compose") +} + +kotlin { + jvmToolchain(libs.versions.jvmVersion.get().toInt()) + + android { + namespace = "io.github.kakaocup.compose.sample.kmp" + compileSdk = libs.versions.compileSdk.get().toInt() + minSdk = libs.versions.minSdk.get().toInt() + } + + listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target -> + target.binaries.framework { + baseName = "SampleKit" + isStatic = true + } + } + + sourceSets { + commonMain.dependencies { + implementation(libs.jetbrains.compose.runtime) + implementation(libs.jetbrains.compose.foundation) + implementation(libs.jetbrains.compose.material) + implementation(libs.jetbrains.compose.ui) + } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.jetbrains.compose.ui.test) + implementation(project(":compose")) + } + } +} diff --git a/sample-kmp/src/androidMain/AndroidManifest.xml b/sample-kmp/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..8072ee00 --- /dev/null +++ b/sample-kmp/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sample-kmp/src/commonMain/kotlin/io/github/kakaocup/compose/sample/MainScreen.kt b/sample-kmp/src/commonMain/kotlin/io/github/kakaocup/compose/sample/MainScreen.kt new file mode 100644 index 00000000..170fcf0d --- /dev/null +++ b/sample-kmp/src/commonMain/kotlin/io/github/kakaocup/compose/sample/MainScreen.kt @@ -0,0 +1,56 @@ +package io.github.kakaocup.compose.sample + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.unit.dp + +@Composable +fun MainScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .semantics { testTag = "MainScreen" } + ) { + Text( + text = "Simple text 1", + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "mySimpleText" } + ) + + Text( + text = "Simple text 2", + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "mySimpleText" } + ) + + Button( + content = { Text(text = "Button 1") }, + onClick = { /* nothing */ }, + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "myTestButton" } + ) + + var count by remember { mutableStateOf(0) } + Button( + content = { Text(text = "$count") }, + onClick = { count += 1 }, + modifier = Modifier + .padding(8.dp) + .semantics { testTag = "clickCounter" } + ) + } +} diff --git a/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/screen/MainActivityScreen.kt b/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/screen/MainActivityScreen.kt new file mode 100644 index 00000000..ff90271a --- /dev/null +++ b/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/screen/MainActivityScreen.kt @@ -0,0 +1,31 @@ +package io.github.kakaocup.compose.screen + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class MainActivityScreen(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( + semanticsProvider = semanticsProvider, + viewBuilderAction = { hasTestTag("MainScreen") } + ) { + + val myText1: KNode = child { + hasTestTag("mySimpleText") + hasPosition(0) + } + + val myText2: KNode = child { + hasTestTag("mySimpleText") + hasPosition(1) + } + + val myButton: KNode = child { + hasTestTag("myTestButton") + hasText("Button 1") + } + + val clickCounter: KNode = child { + hasTestTag("clickCounter") + } +} diff --git a/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/test/SimpleTest.kt b/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/test/SimpleTest.kt new file mode 100644 index 00000000..f5f14b8c --- /dev/null +++ b/sample-kmp/src/commonTest/kotlin/io/github/kakaocup/compose/test/SimpleTest.kt @@ -0,0 +1,39 @@ +package io.github.kakaocup.compose.test + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.runComposeUiTest +import io.github.kakaocup.compose.sample.MainScreen +import io.github.kakaocup.compose.screen.MainActivityScreen +import kotlin.test.Test + +@OptIn(ExperimentalTestApi::class) +class SimpleTest { + + @Test + fun simpleTest() = runComposeUiTest { + setContent { MainScreen() } + + MainActivityScreen(this).apply { + myButton { + assertIsDisplayed() + assertTextContains("Button 1") + } + + myText1 { + assertIsDisplayed() + assertTextContains("Simple text 1") + } + + myText2 { + assertIsDisplayed() + assertTextContains("Simple text 2") + } + + onNode { + hasTestTag("doesNotExist") + }.invoke { + assertDoesNotExist() + } + } + } +} diff --git a/sample-kmp/src/iosMain/kotlin/io/github/kakaocup/compose/sample/MainViewController.kt b/sample-kmp/src/iosMain/kotlin/io/github/kakaocup/compose/sample/MainViewController.kt new file mode 100644 index 00000000..c1f70580 --- /dev/null +++ b/sample-kmp/src/iosMain/kotlin/io/github/kakaocup/compose/sample/MainViewController.kt @@ -0,0 +1,9 @@ +package io.github.kakaocup.compose.sample + +import androidx.compose.ui.window.ComposeUIViewController + +/** + * iOS entry point — consume from an iosApp Xcode project as + * `MainViewControllerKt.MainViewController()`. + */ +fun MainViewController() = ComposeUIViewController { MainScreen() } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index ef7fbefe..1c534a70 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -10,10 +10,6 @@ android { versionCode = 1 versionName = "1.0.0" } - - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.composeCompilerVersion.get() - } packaging { resources { diff --git a/sample/src/androidTest/java/io/github/kakaocup/compose/test/WaitForTest.kt b/sample/src/androidTest/java/io/github/kakaocup/compose/test/WaitForTest.kt index 708a6bbf..04e98373 100644 --- a/sample/src/androidTest/java/io/github/kakaocup/compose/test/WaitForTest.kt +++ b/sample/src/androidTest/java/io/github/kakaocup/compose/test/WaitForTest.kt @@ -2,6 +2,7 @@ package io.github.kakaocup.compose.test import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule +import io.github.kakaocup.compose.node.core.waitUntil import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.rule.KakaoComposeTestRule import io.github.kakaocup.compose.sample.DelayDisplayActivity diff --git a/settings.gradle.kts b/settings.gradle.kts index 5ba91cf6..1a218d0d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ include(":compose-ui") include(":compose-test") include(":compose-semantics") include(":sample") +include(":sample-kmp") pluginManagement { repositories { @@ -24,7 +25,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath("org.gradle.toolchains:foojay-resolver:0.7.0") + classpath("org.gradle.toolchains:foojay-resolver:1.0.0") } }