diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ea445e3..049b74b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ composeMultiplatform = "1.10.0" composeBom = "2025.12.01" coroutines = "1.10.2" kotest = "6.0.7" +testballoon = "1.0.1-K2.3.0" ksp = "2.3.4" robolectric = "4.16" androidxTest = "1.7.0" @@ -29,6 +30,7 @@ kotest-framework-engine = { module = "io.kotest:kotest-framework-engine", versio kotest-assertions-core = { module = "io.kotest:kotest-assertions-core", version.ref = "kotest" } kotest-property = { module = "io.kotest:kotest-property", version.ref = "kotest" } kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref = "kotest" } +testballoon-framework-core = { module = "de.infix.testBalloon:testBalloon-framework-core", version.ref = "testballoon" } junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit5" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTest" } @@ -46,6 +48,7 @@ vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = " spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" } kotest = { id = "io.kotest", version.ref = "kotest" } +testballoon = { id = "de.infix.testBalloon", version.ref = "testballoon" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } android-application = { id = "com.android.application", version.ref = "agp" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } diff --git a/jindong-compose/build.gradle.kts b/jindong-compose/build.gradle.kts index c262565..ff94d25 100644 --- a/jindong-compose/build.gradle.kts +++ b/jindong-compose/build.gradle.kts @@ -21,6 +21,7 @@ plugins { alias(libs.plugins.compose.compiler) alias(libs.plugins.vanniktech.mavenPublish) alias(libs.plugins.kotest) + alias(libs.plugins.testballoon) alias(libs.plugins.ksp) alias(libs.plugins.binaryCompatibilityValidator) } @@ -71,6 +72,8 @@ kotlin { implementation(libs.kotest.assertions.core) implementation(libs.compose.ui.test) implementation(libs.kotlinx.coroutines.test) + // PoC: TestBalloon running alongside Kotest in the same source set. + implementation(libs.testballoon.framework.core) } } } diff --git a/jindong-compose/src/commonTest/kotlin/io/github/compose/jindong/compose/JindongApplierTest.kt b/jindong-compose/src/commonTest/kotlin/io/github/compose/jindong/compose/JindongApplierTest.kt index 19bd5b9..62c4324 100644 --- a/jindong-compose/src/commonTest/kotlin/io/github/compose/jindong/compose/JindongApplierTest.kt +++ b/jindong-compose/src/commonTest/kotlin/io/github/compose/jindong/compose/JindongApplierTest.kt @@ -18,40 +18,38 @@ package io.github.compose.jindong.compose import androidx.compose.runtime.ExperimentalComposeApi +import de.infix.testBalloon.framework.core.testSuite import io.github.compose.jindong.core.element.HapticElement import io.github.compose.jindong.core.element.SequenceElement import io.github.compose.jindong.core.element.VibrationElement import io.github.compose.jindong.core.model.HapticIntensity import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe /** - * Tests for [JindongApplier] using Kotest FunSpec. + * PoC migration of the former Kotest `JindongApplierTest` (FunSpec) to TestBalloon. * - * Verifies correct tree manipulation operations: - * - Insert (top-down and bottom-up) - * - Remove (single and multiple elements) - * - Move (forward, backward, multiple elements) - * - Clear - * - Navigation (down/up) + * The `lateinit var root/applier` + `beforeEach` pattern is replaced by a single `testFixture`. Bound + * with `asContextForEach`, every test — including those in nested suites — receives a freshly built + * [Fixture] as its receiver, so `root`/`applier` read exactly as they did before while the isolation + * is now structural rather than a re-init hook that has to run first. Assertions stay on + * kotest-assertions-core, so only the spec engine changed. + * + * Covers the same tree operations: insert (top-down), remove, move, clear, navigation, edge cases. */ -class JindongApplierTest : - FunSpec({ - - fun createElement(durationMs: Long): VibrationElement = VibrationElement(durationMs = durationMs, intensity = HapticIntensity.HIGH) +val JindongApplierTest by testSuite { - lateinit var root: HapticElement - lateinit var applier: JindongApplier + fun createElement(durationMs: Long): VibrationElement = VibrationElement(durationMs = durationMs, intensity = HapticIntensity.HIGH) - beforeEach { - root = SequenceElement() - applier = JindongApplier(root) - } + class Fixture { + val root: HapticElement = SequenceElement() + val applier = JindongApplier(root) + } - context("JindongApplier insertTopDown") { + testFixture { Fixture() } asContextForEach { + testSuite("JindongApplier insertTopDown") { test("should add element at index 0") { val element = createElement(100) @@ -93,7 +91,7 @@ class JindongApplierTest : } } - context("JindongApplier remove") { + testSuite("JindongApplier remove") { test("should remove single element at index") { val element1 = createElement(100) val element2 = createElement(200) @@ -142,8 +140,8 @@ class JindongApplierTest : } } - context("JindongApplier move") { - context("moving single element") { + testSuite("JindongApplier move") { + testSuite("moving single element") { test("should move element forward") { val element1 = createElement(100) val element2 = createElement(200) @@ -199,7 +197,7 @@ class JindongApplierTest : } } - context("moving multiple elements") { + testSuite("moving multiple elements") { test("should move multiple elements forward") { val elements = (1..5).map { createElement(it * 100L) } elements.forEach { applier.insertTopDown(root.children.size, it) } @@ -240,7 +238,7 @@ class JindongApplierTest : } } - context("complex move scenarios") { + testSuite("complex move scenarios") { test("should handle sequential move operations correctly") { val elements = (1..7).map { createElement(it * 100L) } elements.forEach { applier.insertTopDown(root.children.size, it) } @@ -263,7 +261,7 @@ class JindongApplierTest : } } - context("JindongApplier clear") { + testSuite("JindongApplier clear") { test("should remove all children from root") { val element1 = createElement(100) val element2 = createElement(200) @@ -289,7 +287,7 @@ class JindongApplierTest : } } - context("JindongApplier navigation") { + testSuite("JindongApplier navigation") { test("should navigate down and up correctly") { val containerElement = SequenceElement() val leafElement = createElement(100) @@ -330,7 +328,7 @@ class JindongApplierTest : } } - context("JindongApplier edge cases") { + testSuite("JindongApplier edge cases") { test("should handle empty operations on empty root") { root.children.shouldBeEmpty() @@ -364,4 +362,5 @@ class JindongApplierTest : root.children.shouldBeEmpty() } } - }) + } +} diff --git a/jindong-core/build.gradle.kts b/jindong-core/build.gradle.kts index d4810b4..ecb7117 100644 --- a/jindong-core/build.gradle.kts +++ b/jindong-core/build.gradle.kts @@ -20,6 +20,7 @@ plugins { alias(libs.plugins.android.kotlin.multiplatform.library) alias(libs.plugins.vanniktech.mavenPublish) alias(libs.plugins.kotest) + alias(libs.plugins.testballoon) alias(libs.plugins.ksp) alias(libs.plugins.binaryCompatibilityValidator) alias(libs.plugins.kover) @@ -70,6 +71,8 @@ kotlin { implementation(libs.kotest.framework.engine) implementation(libs.kotest.assertions.core) implementation(libs.kotest.property) + // PoC: TestBalloon running alongside Kotest in the same source set. + implementation(libs.testballoon.framework.core) } named("androidHostTest").dependencies { diff --git a/jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/HapticIntensityTest.kt b/jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/HapticIntensityTest.kt index 3c55c0f..c0c77b6 100644 --- a/jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/HapticIntensityTest.kt +++ b/jindong-core/src/commonTest/kotlin/io/github/compose/jindong/core/model/HapticIntensityTest.kt @@ -15,44 +15,48 @@ */ package io.github.compose.jindong.core.model -import io.kotest.core.spec.style.FunSpec +import de.infix.testBalloon.framework.core.testSuite import io.kotest.matchers.shouldBe -class HapticIntensityTest : - FunSpec({ - context("Pre-defined levels") { - test("LIGHT should have value 0.25") { - HapticIntensity.LIGHT.value shouldBe 0.25f - } - test("MEDIUM should have value 0.5") { - HapticIntensity.MEDIUM.value shouldBe 0.5f - } - test("STRONG should have value 0.75") { - HapticIntensity.STRONG.value shouldBe 0.75f - } - test("HIGH should have value 1.0") { - HapticIntensity.HIGH.value shouldBe 1.0f +/** + * PoC migration of the former Kotest `HapticIntensityTest` (FunSpec) to TestBalloon. + * + * The data-driven cases previously sat as several `shouldBe` calls inside one test body, where the + * first failing input aborts the rest and the test name says nothing about which input broke. Each + * input is now its own test: a plain Kotlin `for` loop over `test(...)` is all TestBalloon needs for + * parameterization. Assertions stay on kotest-assertions-core (`shouldBe`), so only the spec engine + * changed. + */ +val HapticIntensityTest by testSuite { + testSuite("Pre-defined levels") { + val levels = listOf( + Triple("LIGHT", HapticIntensity.LIGHT, 0.25f), + Triple("MEDIUM", HapticIntensity.MEDIUM, 0.5f), + Triple("STRONG", HapticIntensity.STRONG, 0.75f), + Triple("HIGH", HapticIntensity.HIGH, 1.0f), + ) + for ((name, level, expected) in levels) { + test("$name should have value $expected") { + level.value shouldBe expected } } + } - context("Custom intensity") { - test("should coerce values above 1.0 to 1.0") { - HapticIntensity.Custom(1.5f).value shouldBe 1.0f - HapticIntensity.Custom(2.0f).value shouldBe 1.0f - HapticIntensity.Custom(100f).value shouldBe 1.0f + testSuite("Custom intensity") { + for (input in listOf(1.5f, 2.0f, 100f)) { + test("should coerce $input above 1.0 to 1.0") { + HapticIntensity.Custom(input).value shouldBe 1.0f } - - test("should coerce values below 0.0 to 0.0") { - HapticIntensity.Custom(-0.1f).value shouldBe 0.0f - HapticIntensity.Custom(-1.0f).value shouldBe 0.0f - HapticIntensity.Custom(-100f).value shouldBe 0.0f + } + for (input in listOf(-0.1f, -1.0f, -100f)) { + test("should coerce $input below 0.0 to 0.0") { + HapticIntensity.Custom(input).value shouldBe 0.0f } - - test("should preserve valid values") { - HapticIntensity.Custom(0.0f).value shouldBe 0.0f - HapticIntensity.Custom(0.5f).value shouldBe 0.5f - HapticIntensity.Custom(1.0f).value shouldBe 1.0f - HapticIntensity.Custom(0.33f).value shouldBe 0.33f + } + for (input in listOf(0.0f, 0.5f, 1.0f, 0.33f)) { + test("should preserve valid value $input") { + HapticIntensity.Custom(input).value shouldBe input } } - }) + } +}