From 4088bd9d457ecb198b223940333c8127d2d5ea50 Mon Sep 17 00:00:00 2001 From: Aleksei Tiurin Date: Wed, 3 Jul 2024 00:22:49 +0300 Subject: [PATCH 1/4] ios, js, wasm support --- composeApp/build.gradle.kts | 23 ++++--- composeApp/src/commonTest/kotlin/AppTest.kt | 14 +++-- gradle.properties | 1 + iosApp/iosApp.xcodeproj/project.pbxproj | 12 ++-- ultron-common/build.gradle.kts | 43 +++++++++++-- ultron-compose/build.gradle.kts | 45 ++++++++++++-- .../list/UltronComposeListItem.android.kt | 47 ++++++++++++++ .../core/compose/list/UltronComposeList.kt | 34 ++++++++--- .../compose/list/UltronComposeListItem.kt | 61 ++++--------------- .../compose/list/UltronComposeListItem.jvm.kt | 48 +++++++++++++++ .../list/UltronComposeListItem.native.kt | 46 ++++++++++++++ 11 files changed, 285 insertions(+), 89 deletions(-) create mode 100644 ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt create mode 100644 ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt create mode 100644 ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 8d406a78..8e734f2e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -26,22 +26,22 @@ kotlin { dependencies { implementation(libs.androidx.ui.test.junit4.android) debugImplementation(libs.androidx.ui.test.manifest) + implementation(project(":ultron-compose")) } } } jvm("desktop") - -// listOf( -// iosX64(), -// iosArm64(), -// iosSimulatorArm64() -// ).forEach { iosTarget -> -// iosTarget.binaries.framework { -// baseName = "ComposeApp" -// isStatic = true -// } -// } + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } sourceSets { val desktopMain by getting @@ -70,7 +70,6 @@ kotlin { desktopMain.dependencies { implementation(compose.desktop.currentOs) } - // Adds the desktop test dependency val desktopTest by getting { dependencies { implementation(compose.desktop.uiTestJUnit4) diff --git a/composeApp/src/commonTest/kotlin/AppTest.kt b/composeApp/src/commonTest/kotlin/AppTest.kt index 00897c5d..1daf0d51 100644 --- a/composeApp/src/commonTest/kotlin/AppTest.kt +++ b/composeApp/src/commonTest/kotlin/AppTest.kt @@ -1,16 +1,15 @@ import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText -import repositories.ContactRepository import com.atiurin.ultron.core.common.options.TextContainsOption import com.atiurin.ultron.core.compose.list.UltronComposeListItem import com.atiurin.ultron.core.compose.list.composeList import com.atiurin.ultron.core.compose.nodeinteraction.click import com.atiurin.ultron.core.compose.runUltronUiTest import com.atiurin.ultron.extensions.assertIsDisplayed -import com.atiurin.ultron.extensions.click import com.atiurin.ultron.extensions.withAssertion import com.atiurin.ultron.page.Screen +import repositories.ContactRepository import kotlin.test.Test @OptIn(ExperimentalTestApi::class) @@ -20,7 +19,7 @@ class AppTest { setContent { App() } - hasText("Click me!").withAssertion(){ + hasText("Click me!").withAssertion() { hasTestTag("greeting") .assertIsDisplayed() .assertTextContains("Compose: Hello,", option = TextContainsOption(substring = true)) @@ -49,9 +48,14 @@ class AppTest { object ListScreen : Screen() { const val contactsListTestTag = "contactsListTestTag" const val contactsListContentDesc = "contactsListContentDesc" - val list = composeList(hasTestTag(contactsListTestTag)) + val list = composeList( + listMatcher = hasTestTag(contactsListTestTag), +// initBlock = { +// registerItem { ListItem() } +// } + ) - class ListItem : UltronComposeListItem(){ + class ListItem : UltronComposeListItem() { val name by child { hasTestTag("contactNameTestTag") } val status by child { hasTestTag("contactStatusTestTag") } } diff --git a/gradle.properties b/gradle.properties index 6a777cf7..c0bf8685 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,7 @@ android.useAndroidX=true android.nonTransitiveRClass=true org.jetbrains.compose.experimental.wasm.enabled=true org.jetbrains.compose.experimental.jscanvas.enabled=true +org.jetbrains.compose.experimental.macos.enabled=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.mpp.enableCInteropCommonization=true diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index f5bde619..8db87d07 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -17,7 +17,7 @@ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; - 7555FF7B242A565900829871 /* sample-kmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = sample-kmp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF7B242A565900829871 /* sample-kmp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sample-kmp.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; @@ -127,7 +127,7 @@ }; }; buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; - compatibilityVersion = "Xcode 14.0"; + compatibilityVersion = "Xcode 15.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -326,7 +326,7 @@ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -358,7 +358,7 @@ "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", ); INFOPLIST_FILE = iosApp/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.3; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -400,4 +400,4 @@ /* End XCConfigurationList section */ }; rootObject = 7555FF73242A565900829871 /* Project object */; -} \ No newline at end of file +} diff --git a/ultron-common/build.gradle.kts b/ultron-common/build.gradle.kts index e7e3533b..21eda675 100644 --- a/ultron-common/build.gradle.kts +++ b/ultron-common/build.gradle.kts @@ -1,3 +1,7 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -11,15 +15,36 @@ version = project.findProperty("VERSION_NAME")!! kotlin { jvm() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + } androidTarget { publishLibraryVariants("release") - compilations.all { - kotlinOptions { - jvmTarget = "17" - } + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + } + macosX64() + macosArm64() + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true } } + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + js(IR) {} sourceSets { commonMain.dependencies { implementation(libs.okio) @@ -42,6 +67,16 @@ kotlin { implementation(kotlin("stdlib-jdk8")) } } + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + } + } + val wasmJsMain by getting { + dependencies { + implementation(kotlin("stdlib")) + } + } } } diff --git a/ultron-compose/build.gradle.kts b/ultron-compose/build.gradle.kts index f877604a..28b87d56 100644 --- a/ultron-compose/build.gradle.kts +++ b/ultron-compose/build.gradle.kts @@ -1,3 +1,7 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl + plugins { alias(libs.plugins.kotlinMultiplatform) alias(libs.plugins.androidLibrary) @@ -13,14 +17,36 @@ version = project.findProperty("VERSION_NAME")!! kotlin { jvm() + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + } androidTarget { publishLibraryVariants("release") - compilations.all { - kotlinOptions { - jvmTarget = "17" - } + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) } } + @OptIn(ExperimentalKotlinGradlePluginApi::class) + applyDefaultHierarchyTemplate { + } + macosX64() + macosArm64() + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + js(IR) {} sourceSets { commonMain.dependencies { @@ -32,6 +58,7 @@ kotlin { implementation(libs.atomicfu) } val androidMain by getting { +// dependsOn(jvmMain.get()) dependencies { api(project(":ultron-common")) implementation(Libs.androidXRunner) @@ -44,6 +71,16 @@ kotlin { implementation(kotlin("stdlib-jdk8")) } } + val jsMain by getting { + dependencies { + implementation(kotlin("stdlib-js")) + } + } + val wasmJsMain by getting { + dependencies { + implementation(kotlin("stdlib")) + } + } } } diff --git a/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt b/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt new file mode 100644 index 00000000..80519516 --- /dev/null +++ b/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt @@ -0,0 +1,47 @@ +package com.atiurin.ultron.core.compose.list + +import androidx.compose.ui.test.SemanticsMatcher +import com.atiurin.ultron.exceptions.UltronException + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T { + val item = createUltronComposeListItemInstance() + item.setExecutor(ultronComposeList, itemMatcher) + return item +} + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean +): T { + val item = createUltronComposeListItemInstance() + return item +} + +inline fun createUltronComposeListItemInstance(): T { + return try { + T::class.java.newInstance() + } catch (ex: Exception) { + val desc = when { + T::class.isInner -> { + "${T::class.simpleName} is an inner class so you have to delete inner modifier (It is often when kotlin throws 'has no zero argument constructor' but real reason is an inner modifier)" + } + + T::class.constructors.find { it.parameters.isEmpty() } == null -> { + "${T::class.simpleName} doesn't have a constructor without params (create an empty constructor)" + } + + else -> ex.message + } + throw UltronException( + """ + |Couldn't create an instance of ${T::class.simpleName}. + |Possible reason: $desc + |Original exception: ${ex.message}, cause ${ex.cause} + """.trimMargin() + ) + } +} \ No newline at end of file diff --git a/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeList.kt b/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeList.kt index cb836c1d..0547b9cd 100644 --- a/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeList.kt +++ b/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeList.kt @@ -16,15 +16,24 @@ import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.exceptions.UltronAssertionException import com.atiurin.ultron.exceptions.UltronException import com.atiurin.ultron.utils.AssertUtils +import kotlin.reflect.KClass class UltronComposeList( val listMatcher: SemanticsMatcher, var useUnmergedTree: Boolean = true, var positionPropertyKey: SemanticsPropertyKey? = null, + val itemsRegistrator: UltronComposeList.() -> Unit = {}, private val itemSearchLimit: Int = UltronComposeConfig.params.lazyColumnItemSearchLimit, private var operationTimeoutMs: Long = UltronComposeConfig.params.lazyColumnOperationTimeoutMs ) { private val itemChildInteractionProvider = getItemChildInteractionProvider() + val instancesMap = mutableMapOf, () -> UltronComposeListItem>() + inline fun registerItem(noinline creator: () -> T){ + instancesMap[T::class] = creator + } + init { + itemsRegistrator() + } open fun withTimeout(timeoutMs: Long) = UltronComposeList( @@ -45,8 +54,11 @@ class UltronComposeList( if (positionPropertyKey == null) { throw UltronException( """ - |[positionPropertyKey] parameter is not specified for Compose List - |Configure it by using [composeList(.., positionPropertyKey = ListItemPositionPropertyKey)] + |[positionPropertyKey] parameter is not specified for UltronComposeList + |Configure it like + |``` + |composeList(.., positionPropertyKey = ListItemPositionPropertyKey) + |``` """.trimMargin() ) } @@ -101,19 +113,22 @@ class UltronComposeList( ) inline fun getItem(matcher: SemanticsMatcher): T { - return UltronComposeListItem.getInstance(this, matcher) + return getComposeListItemInstance(this, matcher) } inline fun getItem(position: Int): T { if (positionPropertyKey == null) { throw UltronException( """ - |[positionPropertyKey] parameter is not specified for Compose List - |Configure it by using [composeList(.., positionPropertyKey = ListItemPositionPropertyKey)] + |[positionPropertyKey] parameter is not specified for UltronComposeList + |Configure it like + |``` + |composeList(.., positionPropertyKey = ListItemPositionPropertyKey) + |``` """.trimMargin() ) } - return UltronComposeListItem.getInstance(this, position, true) + return getComposeListItemInstance(this, position, true) } inline fun getFirstItem(): T = getItem(0) @@ -123,7 +138,7 @@ class UltronComposeList( * After scroll the positions of children inside the list are changed. */ inline fun getVisibleItem(index: Int): T { - return UltronComposeListItem.getInstance(this, index) + return getComposeListItemInstance(this, index) } /** @@ -277,5 +292,6 @@ class UltronComposeList( fun composeList( listMatcher: SemanticsMatcher, useUnmergedTree: Boolean = true, - positionPropertyKey: SemanticsPropertyKey? = null -) = UltronComposeList(listMatcher, useUnmergedTree, positionPropertyKey) + positionPropertyKey: SemanticsPropertyKey? = null, + initBlock: UltronComposeList.() -> Unit = {} +) = UltronComposeList(listMatcher, useUnmergedTree, positionPropertyKey, initBlock) diff --git a/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt b/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt index 6e9f6f4c..f2b6a82d 100644 --- a/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt +++ b/ultron-compose/src/commonMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.kt @@ -49,7 +49,6 @@ import com.atiurin.ultron.core.compose.operation.ComposeOperationResult import com.atiurin.ultron.core.compose.operation.UltronComposeOperation import com.atiurin.ultron.core.compose.operation.UltronComposeOperationParams import com.atiurin.ultron.core.compose.option.ComposeSwipeOption -import com.atiurin.ultron.exceptions.UltronException open class UltronComposeListItem { lateinit var executor: ComposeItemExecutor @@ -191,51 +190,15 @@ open class UltronComposeListItem { fun assertTextEquals(expected: String) = apply { getItemUltronComposeInteraction().assertTextEquals(expected) } fun assertTextContains(expected: String) = apply { getItemUltronComposeInteraction().assertTextContains(expected) } - - - companion object { - inline fun getInstance( - ultronComposeList: UltronComposeList, - itemMatcher: SemanticsMatcher - ): T { - val item = createUltronComposeListItemInstance() - item.setExecutor(ultronComposeList, itemMatcher) - return item - } - - inline fun getInstance( - ultronComposeList: UltronComposeList, - position: Int, - isPositionPropertyConfigured: Boolean = false - ): T { - val item = createUltronComposeListItemInstance() - item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) - return item - } - - inline fun createUltronComposeListItemInstance(): T { - return try { - T::class.java.newInstance() - } catch (ex: Exception) { - val desc = when { - T::class.isInner -> { - "${T::class.simpleName} is an inner class so you have to delete inner modifier (It is often when kotlin throws 'has no zero argument constructor' but real reason is an inner modifier)" - } - - T::class.constructors.find { it.parameters.isEmpty() } == null -> { - "${T::class.simpleName} doesn't have a constructor without params (create an empty constructor)" - } - - else -> ex.message - } - throw UltronException( - """ - |Couldn't create an instance of ${T::class.simpleName}. - |Possible reason: $desc - |Original exception: ${ex.message}, cause ${ex.cause} - """.trimMargin() - ) - } - } - } -} \ No newline at end of file +} + +expect inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T + +expect inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean = false +): T \ No newline at end of file diff --git a/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt b/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt new file mode 100644 index 00000000..ea8aa8db --- /dev/null +++ b/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt @@ -0,0 +1,48 @@ +package com.atiurin.ultron.core.compose.list + + +import androidx.compose.ui.test.SemanticsMatcher +import com.atiurin.ultron.exceptions.UltronException + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T { + val item = createUltronComposeListItemInstance() + item.setExecutor(ultronComposeList, itemMatcher) + return item +} + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean +): T { + val item = createUltronComposeListItemInstance() + return item +} + +inline fun createUltronComposeListItemInstance(): T { + return try { + T::class.java.newInstance() + } catch (ex: Exception) { + val desc = when { + T::class.isInner -> { + "${T::class.simpleName} is an inner class so you have to delete inner modifier (It is often when kotlin throws 'has no zero argument constructor' but real reason is an inner modifier)" + } + + T::class.constructors.find { it.parameters.isEmpty() } == null -> { + "${T::class.simpleName} doesn't have a constructor without params (create an empty constructor)" + } + + else -> ex.message + } + throw UltronException( + """ + |Couldn't create an instance of ${T::class.simpleName}. + |Possible reason: $desc + |Original exception: ${ex.message}, cause ${ex.cause} + """.trimMargin() + ) + } +} \ No newline at end of file diff --git a/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt b/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt new file mode 100644 index 00000000..7702b456 --- /dev/null +++ b/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt @@ -0,0 +1,46 @@ +package com.atiurin.ultron.core.compose.list + +import androidx.compose.ui.test.SemanticsMatcher +import com.atiurin.ultron.exceptions.UltronException +import kotlin.reflect.KClass + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, itemMatcher) + return item as T +} + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) + return item as T +} \ No newline at end of file From 872d9587ef816b6a1509a6d595317ff7524a6e19 Mon Sep 17 00:00:00 2001 From: Aleksei Tiurin Date: Wed, 3 Jul 2024 01:23:58 +0300 Subject: [PATCH 2/4] js and wasmJs support --- composeApp/build.gradle.kts | 8 +++- composeApp/src/commonTest/kotlin/AppTest.kt | 6 +-- ultron-common/build.gradle.kts | 10 ++--- ultron-compose/build.gradle.kts | 2 +- .../compose/list/UltronComposeListItem.js.kt | 45 +++++++++++++++++++ .../list/UltronComposeListItem.native.kt | 1 - .../list/UltronComposeListItem.wasmJs.kt | 45 +++++++++++++++++++ 7 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 ultron-compose/src/jsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.js.kt create mode 100644 ultron-compose/src/wasmJsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.wasmJs.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 8e734f2e..0e2b968f 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -42,7 +42,10 @@ kotlin { isStatic = true } } - + js(IR) + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + sourceSets { val desktopMain by getting @@ -76,6 +79,9 @@ kotlin { implementation(compose.desktop.currentOs) } } + @OptIn(ExperimentalWasmDsl::class) + wasmJs() + val wasmJsTest by getting } } diff --git a/composeApp/src/commonTest/kotlin/AppTest.kt b/composeApp/src/commonTest/kotlin/AppTest.kt index 1daf0d51..af42264f 100644 --- a/composeApp/src/commonTest/kotlin/AppTest.kt +++ b/composeApp/src/commonTest/kotlin/AppTest.kt @@ -50,9 +50,9 @@ object ListScreen : Screen() { const val contactsListContentDesc = "contactsListContentDesc" val list = composeList( listMatcher = hasTestTag(contactsListTestTag), -// initBlock = { -// registerItem { ListItem() } -// } + initBlock = { + registerItem { ListItem() } + } ) class ListItem : UltronComposeListItem() { diff --git a/ultron-common/build.gradle.kts b/ultron-common/build.gradle.kts index 21eda675..439a3857 100644 --- a/ultron-common/build.gradle.kts +++ b/ultron-common/build.gradle.kts @@ -14,11 +14,12 @@ group = project.findProperty("GROUP")!! version = project.findProperty("VERSION_NAME")!! kotlin { - jvm() @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) } + // targets + jvm() androidTarget { publishLibraryVariants("release") @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -26,9 +27,7 @@ kotlin { jvmTarget.set(JvmTarget.JVM_17) } } - @OptIn(ExperimentalKotlinGradlePluginApi::class) - applyDefaultHierarchyTemplate { - } + applyDefaultHierarchyTemplate() macosX64() macosArm64() listOf( @@ -41,10 +40,9 @@ kotlin { isStatic = true } } - @OptIn(ExperimentalWasmDsl::class) wasmJs() - js(IR) {} + js(IR) sourceSets { commonMain.dependencies { implementation(libs.okio) diff --git a/ultron-compose/build.gradle.kts b/ultron-compose/build.gradle.kts index 28b87d56..48281958 100644 --- a/ultron-compose/build.gradle.kts +++ b/ultron-compose/build.gradle.kts @@ -46,7 +46,7 @@ kotlin { @OptIn(ExperimentalWasmDsl::class) wasmJs() - js(IR) {} + js(IR) sourceSets { commonMain.dependencies { diff --git a/ultron-compose/src/jsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.js.kt b/ultron-compose/src/jsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.js.kt new file mode 100644 index 00000000..4886666f --- /dev/null +++ b/ultron-compose/src/jsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.js.kt @@ -0,0 +1,45 @@ +package com.atiurin.ultron.core.compose.list + +import androidx.compose.ui.test.SemanticsMatcher +import com.atiurin.ultron.exceptions.UltronException + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, itemMatcher) + return item as T +} + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) + return item as T +} \ No newline at end of file diff --git a/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt b/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt index 7702b456..4886666f 100644 --- a/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt +++ b/ultron-compose/src/nativeMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.native.kt @@ -2,7 +2,6 @@ package com.atiurin.ultron.core.compose.list import androidx.compose.ui.test.SemanticsMatcher import com.atiurin.ultron.exceptions.UltronException -import kotlin.reflect.KClass actual inline fun getComposeListItemInstance( ultronComposeList: UltronComposeList, diff --git a/ultron-compose/src/wasmJsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.wasmJs.kt b/ultron-compose/src/wasmJsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.wasmJs.kt new file mode 100644 index 00000000..4886666f --- /dev/null +++ b/ultron-compose/src/wasmJsMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.wasmJs.kt @@ -0,0 +1,45 @@ +package com.atiurin.ultron.core.compose.list + +import androidx.compose.ui.test.SemanticsMatcher +import com.atiurin.ultron.exceptions.UltronException + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + itemMatcher: SemanticsMatcher +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, itemMatcher) + return item as T +} + +actual inline fun getComposeListItemInstance( + ultronComposeList: UltronComposeList, + position: Int, + isPositionPropertyConfigured: Boolean +): T { + val item = ultronComposeList.instancesMap[T::class]?.invoke() + ?: throw UltronException( + """ + |Item with ${T::class} not registered in compose list ${ultronComposeList.listMatcher.description} + |Configure it by using [initBlock] parameter + |``` + |composeList(.., initBlock = { + | registerItem { ListItem() } + |}) + |``` + """.trimMargin() + ) + item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) + return item as T +} \ No newline at end of file From e6be809a5d88114a89f45a3a9466270416f01e22 Mon Sep 17 00:00:00 2001 From: Aleksei Tiurin Date: Wed, 3 Jul 2024 02:40:09 +0300 Subject: [PATCH 3/4] Fix android and jvm composeList --- composeApp/build.gradle.kts | 1 - composeApp/src/commonTest/kotlin/AppTest.kt | 9 ++++++++- .../com/atiurin/sampleapp/framework/utils/AssertUtils.kt | 2 +- ultron-common/build.gradle.kts | 2 +- .../core/compose/list/UltronComposeListItem.android.kt | 1 + .../core/compose/list/UltronComposeListItem.jvm.kt | 1 + 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 0e2b968f..2f1d7f50 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -4,7 +4,6 @@ import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl -import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig plugins { alias(libs.plugins.kotlinMultiplatform) diff --git a/composeApp/src/commonTest/kotlin/AppTest.kt b/composeApp/src/commonTest/kotlin/AppTest.kt index af42264f..80c0989a 100644 --- a/composeApp/src/commonTest/kotlin/AppTest.kt +++ b/composeApp/src/commonTest/kotlin/AppTest.kt @@ -31,10 +31,17 @@ class AppTest { setContent { App() } - val contact = ContactRepository.getFirst() composeList(hasTestTag("contactsListTestTag")) .assertIsDisplayed().assertNotEmpty() .firstVisibleItem().assertIsDisplayed() + } + + @Test + fun testListItemChildElements() = runUltronUiTest { + setContent { + App() + } + val contact = ContactRepository.getFirst() ListScreen { list.assertContentDescriptionEquals(contactsListContentDesc) list.getFirstVisibleItem().apply { diff --git a/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt b/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt index 9633efd2..4eaa1d18 100644 --- a/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt +++ b/sample-app/src/androidTest/java/com/atiurin/sampleapp/framework/utils/AssertUtils.kt @@ -8,7 +8,7 @@ object AssertUtils { try { block() } catch (ex: Throwable) { - throw ex +// throw ex exceptionOccurs = true } Assert.assertEquals(expected, exceptionOccurs) diff --git a/ultron-common/build.gradle.kts b/ultron-common/build.gradle.kts index 439a3857..eef7062a 100644 --- a/ultron-common/build.gradle.kts +++ b/ultron-common/build.gradle.kts @@ -42,7 +42,7 @@ kotlin { } @OptIn(ExperimentalWasmDsl::class) wasmJs() - js(IR) + js(IR) sourceSets { commonMain.dependencies { implementation(libs.okio) diff --git a/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt b/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt index 80519516..712f4a71 100644 --- a/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt +++ b/ultron-compose/src/androidMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.android.kt @@ -18,6 +18,7 @@ actual inline fun getComposeListItemInstance isPositionPropertyConfigured: Boolean ): T { val item = createUltronComposeListItemInstance() + item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) return item } diff --git a/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt b/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt index ea8aa8db..800f4489 100644 --- a/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt +++ b/ultron-compose/src/jvmMain/kotlin/com/atiurin/ultron/core/compose/list/UltronComposeListItem.jvm.kt @@ -19,6 +19,7 @@ actual inline fun getComposeListItemInstance isPositionPropertyConfigured: Boolean ): T { val item = createUltronComposeListItemInstance() + item.setExecutor(ultronComposeList, position, isPositionPropertyConfigured) return item } From 4a83c2ae0f601248f123e2ed0ba57f2e0ebbb0d5 Mon Sep 17 00:00:00 2001 From: Aleksei Tiurin Date: Wed, 3 Jul 2024 02:43:27 +0300 Subject: [PATCH 4/4] 2.5.0-alpha06 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c0bf8685..e2448728 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,4 +14,4 @@ kotlin.mpp.enableCInteropCommonization=true GROUP=com.atiurin POM_ARTIFACT_ID=ultron -VERSION_NAME=2.5.0-alpha05 +VERSION_NAME=2.5.0-alpha06