Skip to content

Commit f3fd95f

Browse files
committed
refactor: Introduce Idling Resource for UI tests
- Introduce a `CountingIdlingRes` object to track asynchronous operations and ensure UI tests wait for them to complete. This is similar to Espresso's `CountingIdlingResource`. - Create a `ComposeCountingIdlingResource` wrapper to integrate `CountingIdlingRes` with Compose UI tests. - Register and unregister the idling resource in `AbstractJvmUiTests` during `setUp` and `tearDown`. - Update `SettingsViewModel` to increment/decrement the `CountingIdlingRes` counter during long-running asynchronous operations like encryption changes and database path retrieval. - Override `setUp` and `tearDown` methods in `AndroidUiTests.kt` and `DesktopUiTests.kt` to call the superclass implementations, ensuring proper registration of the idling resource.
1 parent 0997b06 commit f3fd95f

File tree

6 files changed

+136
-1
lines changed

6 files changed

+136
-1
lines changed

app/android/src/androidTest/java/com/softartdev/notedelight/ui/AndroidUiTests.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import androidx.test.filters.LargeTest
88
import com.softartdev.notedelight.MainActivity
99
import leakcanary.DetectLeaksAfterTestSuccess
1010
import leakcanary.TestDescriptionHolder
11+
import org.junit.After
12+
import org.junit.Before
1113
import org.junit.Rule
1214
import org.junit.Test
1315
import org.junit.rules.RuleChain
@@ -25,6 +27,12 @@ class AndroidUiTests : AbstractJvmUiTests() {
2527
.around(DetectLeaksAfterTestSuccess())
2628
.around(composeTestRule)
2729

30+
@Before
31+
override fun setUp() = super.setUp()
32+
33+
@After
34+
override fun tearDown() = super.tearDown()
35+
2836
@Test
2937
override fun crudNoteTest() = super.crudNoteTest()
3038

app/desktop/src/jvmTest/kotlin/com/softartdev/notedelight/ui/DesktopUiTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class DesktopUiTests : AbstractJvmUiTests() {
4343

4444
@Before
4545
override fun setUp() = runTest {
46+
super.setUp()
4647
Logger.setLogWriters(platformLogWriter())
4748
when (GlobalContext.getKoinApplicationOrNull()) {
4849
null -> startKoin {
@@ -57,7 +58,6 @@ class DesktopUiTests : AbstractJvmUiTests() {
5758
safeRepo.buildDbIfNeed()
5859
val noteDAO: NoteDAO = get(NoteDAO::class.java)
5960
noteDAO.deleteAll()
60-
super.setUp()
6161
val lifecycleOwner = TestLifecycleOwner(
6262
initialState = Lifecycle.State.RESUMED,
6363
coroutineDispatcher = Dispatchers.Swing
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.softartdev.notedelight.util
2+
3+
import co.touchlab.kermit.Logger
4+
import kotlinx.coroutines.sync.Mutex
5+
import kotlinx.coroutines.sync.withLock
6+
7+
/**
8+
* An object that determines idleness by maintaining an internal counter.
9+
* When the counter is 0 - it is considered to be idle, when it is non-zero it is not idle.
10+
* This is very similar to the way a {@link java.util.concurrent.Semaphore} behaves.
11+
*
12+
* <p>The counter may be incremented or decremented from any thread. If it reaches an illogical
13+
* state (like counter less than zero) it will throw an IllegalStateException.
14+
*
15+
* <p>This class can then be used to wrap up operations that while in progress should block tests
16+
* from accessing the UI.
17+
*
18+
* <pre>{@code
19+
* public interface FooServer {
20+
* public Foo newFoo();
21+
* public void updateFoo(Foo foo);
22+
* }
23+
*
24+
* public DecoratedFooServer implements FooServer {
25+
* private final FooServer realFooServer;
26+
* private final CountingIdlingRes fooServerIdlingResource;
27+
*
28+
* public DecoratedFooServer(FooServer realFooServer,
29+
* CountingIdlingRes fooServerIdlingResource) {
30+
* this.realFooServer = checkNotNull(realFooServer);
31+
* this.fooServerIdlingResource = checkNotNull(fooServerIdlingResource);
32+
* }
33+
*
34+
* public Foo newFoo() {
35+
* fooServerIdlingResource.increment();
36+
* try {
37+
* return realFooServer.newFoo();
38+
* } finally {
39+
* fooServerIdlingResource.decrement();
40+
* }
41+
* }
42+
*
43+
* public void updateFoo(Foo foo) {
44+
* fooServerIdlingResource.increment();
45+
* try {
46+
* realFooServer.updateFoo(foo);
47+
* } finally {
48+
* fooServerIdlingResource.decrement();
49+
* }
50+
* }
51+
* }
52+
* }</pre>
53+
*
54+
* Then in your test setup:
55+
*
56+
* <pre>{@code
57+
* public void setUp() throws Exception {
58+
* super.setUp();
59+
* FooServer realServer = FooApplication.getFooServer();
60+
* CountingIdlingRes countingResource = new CountingIdlingRes("FooServerCalls");
61+
* FooApplication.setFooServer(new DecoratedFooServer(realServer, countingResource));
62+
* Espresso.registerIdlingResource(countingResource);
63+
* }
64+
* }</pre>
65+
*/
66+
object CountingIdlingRes {
67+
private val logger = Logger.withTag("CountingIdlingRes")
68+
private val mutex = Mutex()
69+
70+
var counter: Int = 0
71+
private set
72+
73+
val isIdleNow: Boolean
74+
get() = counter == 0
75+
76+
suspend fun increment() = mutex.withLock(action = ::unsafeIncrement)
77+
78+
suspend fun decrement() = mutex.withLock(action = ::unsafeDecrement)
79+
80+
private fun unsafeIncrement() {
81+
counter++
82+
logger.d { "IdlingResource incremented, counter = $counter" }
83+
}
84+
85+
private fun unsafeDecrement() {
86+
if (counter <= 0) {
87+
logger.e { "IdlingResource counter has been corrupted! Counter = $counter" }
88+
return
89+
}
90+
counter--
91+
logger.d { "IdlingResource decremented, counter = $counter" }
92+
}
93+
}

core/presentation/src/commonMain/kotlin/com/softartdev/notedelight/presentation/settings/SettingsViewModel.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.softartdev.notedelight.navigation.Router
1212
import com.softartdev.notedelight.repository.SafeRepo
1313
import com.softartdev.notedelight.usecase.crypt.CheckSqlCipherVersionUseCase
1414
import com.softartdev.notedelight.usecase.settings.RevealFileListUseCase
15+
import com.softartdev.notedelight.util.CountingIdlingRes
1516
import kotlinx.coroutines.flow.MutableStateFlow
1617
import kotlinx.coroutines.flow.StateFlow
1718
import kotlinx.coroutines.flow.update
@@ -58,6 +59,7 @@ class SettingsViewModel(
5859
private fun changeLanguage() = router.navigate(route = AppNavGraph.LanguageDialog)
5960

6061
private fun checkEncryption() = viewModelScope.launch {
62+
CountingIdlingRes.increment()
6163
mutableStateFlow.update(SecurityResult::showLoading)
6264
try {
6365
mutableStateFlow.update { result ->
@@ -70,10 +72,12 @@ class SettingsViewModel(
7072
handleError(e) { "error checking encryption" }
7173
} finally {
7274
mutableStateFlow.update(SecurityResult::hideLoading)
75+
CountingIdlingRes.decrement()
7376
}
7477
}
7578

7679
private fun changeEncryption(checked: Boolean) = viewModelScope.launch {
80+
CountingIdlingRes.increment()
7781
mutableStateFlow.update(SecurityResult::showLoading)
7882
try {
7983
when {
@@ -87,10 +91,12 @@ class SettingsViewModel(
8791
handleError(e) { "error changing encryption" }
8892
} finally {
8993
mutableStateFlow.update(SecurityResult::hideLoading)
94+
CountingIdlingRes.decrement()
9095
}
9196
}
9297

9398
private fun changePassword() = viewModelScope.launch {
99+
CountingIdlingRes.increment()
94100
mutableStateFlow.update(SecurityResult::showLoading)
95101
try {
96102
when {
@@ -101,10 +107,12 @@ class SettingsViewModel(
101107
handleError(e) { "error changing password" }
102108
} finally {
103109
mutableStateFlow.update(SecurityResult::hideLoading)
110+
CountingIdlingRes.decrement()
104111
}
105112
}
106113

107114
private fun showCipherVersion() = viewModelScope.launch {
115+
CountingIdlingRes.increment()
108116
mutableStateFlow.update(SecurityResult::showLoading)
109117
try {
110118
val cipherVersion: String? = checkSqlCipherVersionUseCase.invoke()
@@ -113,10 +121,12 @@ class SettingsViewModel(
113121
handleError(e) { "error checking sqlcipher version" }
114122
} finally {
115123
mutableStateFlow.update(SecurityResult::hideLoading)
124+
CountingIdlingRes.decrement()
116125
}
117126
}
118127

119128
private fun showDatabasePath() = viewModelScope.launch {
129+
CountingIdlingRes.increment()
120130
mutableStateFlow.update(SecurityResult::showLoading)
121131
try {
122132
val dbPath: String = safeRepo.dbPath
@@ -125,6 +135,7 @@ class SettingsViewModel(
125135
handleError(e) { "error getting database path" }
126136
} finally {
127137
mutableStateFlow.update(SecurityResult::hideLoading)
138+
CountingIdlingRes.decrement()
128139
}
129140
}
130141

ui/test-jvm/src/main/kotlin/com/softartdev/notedelight/ui/AbstractJvmUiTests.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@ import androidx.compose.ui.test.ExperimentalTestApi
77
import androidx.compose.ui.test.junit4.ComposeContentTestRule
88
import com.softartdev.notedelight.AbstractUITests
99
import com.softartdev.notedelight.reflect
10+
import com.softartdev.notedelight.util.ComposeCountingIdlingResource
1011

1112
abstract class AbstractJvmUiTests : AbstractUITests() {
1213
abstract val composeTestRule: ComposeContentTestRule
1314
override val composeUiTest: ComposeUiTest by lazy { reflect(composeTestRule) }
15+
16+
override fun setUp() {
17+
super.setUp()
18+
composeTestRule.registerIdlingResource(ComposeCountingIdlingResource)
19+
}
20+
21+
override fun tearDown() {
22+
super.tearDown()
23+
composeTestRule.unregisterIdlingResource(ComposeCountingIdlingResource)
24+
}
1425
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.softartdev.notedelight.util
2+
3+
import androidx.compose.ui.test.IdlingResource
4+
5+
object ComposeCountingIdlingResource : IdlingResource {
6+
7+
override val isIdleNow: Boolean
8+
get() = CountingIdlingRes.isIdleNow
9+
10+
override fun getDiagnosticMessageIfBusy(): String =
11+
"Idling resource with counter = ${CountingIdlingRes.counter}"
12+
}

0 commit comments

Comments
 (0)