Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" }
Expand All @@ -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" }
Expand Down
3 changes: 3 additions & 0 deletions jindong-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -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) }
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -364,4 +362,5 @@ class JindongApplierTest :
root.children.shouldBeEmpty()
}
}
})
}
}
3 changes: 3 additions & 0 deletions jindong-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
})
}
}
Loading