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")
}
}