Skip to content

Commit ba2646b

Browse files
committed
test: add HapticManagerTest with fake object
1 parent 90630a6 commit ba2646b

3 files changed

Lines changed: 245 additions & 3 deletions

File tree

jindong-core/src/commonMain/kotlin/io/github/compose/jindong/core/HapticManager.kt

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,12 @@ object HapticManager {
9595
* On iOS, this is a no-op as no context is needed.
9696
*
9797
* @param context Platform-specific context (Android: Context, iOS: null)
98+
* @param executor Platform-specific executor
9899
*/
99-
fun initialize(context: Any? = null) {
100+
fun initialize(context: Any? = null, executor: HapticExecutor = createHapticExecutor(context)) {
100101
withStateLockBlocking {
101-
if (executor == null) {
102-
executor = createHapticExecutor(context)
102+
if (this.executor == null) {
103+
this.executor = executor
103104
}
104105
}
105106
}
@@ -172,6 +173,20 @@ object HapticManager {
172173
}
173174
}
174175

176+
/**
177+
* **⚠️ Testing only**: Sets a custom executor without going through platform initialization.
178+
*
179+
* This function is only intended for use in unit tests. Do not use in production code.
180+
* The provided executor must be properly configured.
181+
*
182+
* @param testExecutor The custom executor to use for testing
183+
*/
184+
fun setExecutorForTesting(testExecutor: HapticExecutor) {
185+
withStateLockBlocking {
186+
executor = testExecutor
187+
}
188+
}
189+
175190
private suspend fun <T> withStateLock(action: () -> T): T = stateMutex.withLock { action() }
176191

177192
private fun <T> withStateLockBlocking(action: () -> T): T = runBlocking {
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (C) 2026 compose-jindong
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.compose.jindong.core
17+
18+
import io.github.compose.jindong.core.dsl.buildHapticPattern
19+
import io.github.compose.jindong.core.fake.FakeHapticExecutor
20+
import io.github.compose.jindong.core.model.HapticIntensity
21+
import io.kotest.assertions.assertSoftly
22+
import io.kotest.assertions.throwables.shouldNotThrowAny
23+
import io.kotest.core.spec.style.FunSpec
24+
import io.kotest.matchers.collections.shouldBeEmpty
25+
import io.kotest.matchers.collections.shouldHaveSize
26+
import io.kotest.matchers.shouldBe
27+
28+
class HapticManagerTest :
29+
FunSpec({
30+
lateinit var fakeExecutor: FakeHapticExecutor
31+
32+
beforeEach {
33+
fakeExecutor = FakeHapticExecutor()
34+
HapticManager.setExecutorForTesting(fakeExecutor)
35+
}
36+
37+
afterEach {
38+
HapticManager.release()
39+
}
40+
41+
test("isSupported should return true when executor supports haptics") {
42+
HapticManager.isSupported shouldBe true
43+
}
44+
45+
test("isSupported should return false when executor does not support haptics") {
46+
HapticManager.setExecutorForTesting(FakeHapticExecutor(isSupported = false))
47+
48+
HapticManager.isSupported shouldBe false
49+
}
50+
51+
test("execute should execute pattern synchronously") {
52+
val pattern = buildHapticPattern {
53+
haptic(100.ms)
54+
}
55+
56+
HapticManager.execute(pattern)
57+
58+
assertSoftly {
59+
fakeExecutor.executedPatterns shouldHaveSize 1
60+
fakeExecutor.executedPatterns[0] shouldBe pattern
61+
}
62+
}
63+
64+
test("execute should handle empty pattern") {
65+
val emptyPattern = buildHapticPattern { }
66+
67+
HapticManager.execute(emptyPattern)
68+
69+
assertSoftly {
70+
fakeExecutor.executedPatterns shouldHaveSize 1
71+
fakeExecutor.executedPatterns[0].events.shouldBeEmpty()
72+
}
73+
}
74+
75+
test("executeAsync should execute pattern and return handle") {
76+
val pattern = buildHapticPattern {
77+
haptic(100.ms, HapticIntensity.STRONG)
78+
}
79+
80+
val handle = HapticManager.executeAsync(pattern)
81+
82+
assertSoftly {
83+
fakeExecutor.asyncExecutedPatterns shouldHaveSize 1
84+
fakeExecutor.asyncExecutedPatterns[0] shouldBe pattern
85+
handle.isActive shouldBe true
86+
}
87+
}
88+
89+
test("executeAsync should cancel previous handle when executing new pattern") {
90+
val pattern1 = buildHapticPattern { haptic(100.ms) }
91+
val pattern2 = buildHapticPattern { haptic(200.ms) }
92+
93+
val handle1 = HapticManager.executeAsync(pattern1)
94+
HapticManager.executeAsync(pattern2)
95+
96+
assertSoftly {
97+
fakeExecutor.asyncExecutedPatterns shouldHaveSize 2
98+
handle1.isActive shouldBe false
99+
}
100+
}
101+
102+
test("cancel should cancel current execution") {
103+
val pattern = buildHapticPattern { haptic(100.ms) }
104+
val handle = HapticManager.executeAsync(pattern)
105+
106+
HapticManager.cancel()
107+
108+
handle.isActive shouldBe false
109+
}
110+
111+
test("cancel should not throw when no active execution exists") {
112+
shouldNotThrowAny {
113+
HapticManager.cancel()
114+
}
115+
}
116+
117+
test("release should release executor resources") {
118+
HapticManager.release()
119+
120+
fakeExecutor.releaseCalled shouldBe true
121+
}
122+
123+
test("release should cancel active execution") {
124+
val pattern = buildHapticPattern { haptic(100.ms) }
125+
val handle = HapticManager.executeAsync(pattern)
126+
127+
HapticManager.release()
128+
129+
assertSoftly {
130+
handle.isActive shouldBe false
131+
fakeExecutor.releaseCalled shouldBe true
132+
}
133+
}
134+
135+
test("should allow reuse after release") {
136+
val firstExecutor = FakeHapticExecutor()
137+
HapticManager.setExecutorForTesting(firstExecutor)
138+
HapticManager.release()
139+
140+
val secondExecutor = FakeHapticExecutor()
141+
HapticManager.setExecutorForTesting(secondExecutor)
142+
val pattern = buildHapticPattern { haptic(100.ms) }
143+
HapticManager.executeAsync(pattern)
144+
145+
assertSoftly {
146+
firstExecutor.releaseCalled shouldBe true
147+
firstExecutor.asyncExecutedPatterns.shouldBeEmpty()
148+
secondExecutor.asyncExecutedPatterns shouldHaveSize 1
149+
}
150+
}
151+
})
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright (C) 2026 compose-jindong
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.github.compose.jindong.core.fake
17+
18+
import io.github.compose.jindong.core.executor.HapticExecutor
19+
import io.github.compose.jindong.core.executor.HapticHandle
20+
import io.github.compose.jindong.core.model.HapticPattern
21+
import kotlinx.coroutines.delay
22+
23+
/**
24+
* Fake implementation of [HapticExecutor] for testing.
25+
*
26+
* Records all executed patterns and simulates execution delays.
27+
*/
28+
class FakeHapticExecutor(
29+
override val isSupported: Boolean = true,
30+
) : HapticExecutor {
31+
32+
private val _executedPatterns = mutableListOf<HapticPattern>()
33+
private val _asyncExecutedPatterns = mutableListOf<HapticPattern>()
34+
private var _releaseCalled = false
35+
36+
val executedPatterns: List<HapticPattern> get() = _executedPatterns.toList()
37+
val asyncExecutedPatterns: List<HapticPattern> get() = _asyncExecutedPatterns.toList()
38+
val releaseCalled: Boolean get() = _releaseCalled
39+
40+
override suspend fun execute(pattern: HapticPattern) {
41+
_executedPatterns.add(pattern)
42+
// Simulate minimal execution time
43+
delay(1)
44+
}
45+
46+
override fun executeAsync(pattern: HapticPattern): HapticHandle {
47+
_asyncExecutedPatterns.add(pattern)
48+
return FakeHapticHandle()
49+
}
50+
51+
override fun release() {
52+
_releaseCalled = true
53+
}
54+
55+
fun reset() {
56+
_executedPatterns.clear()
57+
_asyncExecutedPatterns.clear()
58+
_releaseCalled = false
59+
}
60+
}
61+
62+
/**
63+
* Fake implementation of [HapticHandle] for testing.
64+
*/
65+
class FakeHapticHandle : HapticHandle {
66+
private var _cancelled = false
67+
private var _isActive = true
68+
69+
val cancelled: Boolean get() = _cancelled
70+
override val isActive: Boolean get() = _isActive && !_cancelled
71+
72+
override fun cancel() {
73+
_cancelled = true
74+
_isActive = false
75+
}
76+
}

0 commit comments

Comments
 (0)