Refactor code structure for improved readability and maintainability
This commit is contained in:
9
android-app/app/src/androidTest/AndroidManifest.xml
Normal file
9
android-app/app/src/androidTest/AndroidManifest.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:name="dagger.hilt.android.testing.HiltTestApplication"
|
||||
android:allowBackup="false">
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,6 @@
|
||||
// Disabled TestBindingsModule — replaced by TestHiltModules.kt
|
||||
// Kept as an empty placeholder to avoid accidental compilation of the previous
|
||||
// broken test module. Refer to TestHiltModules.kt for test bindings.
|
||||
package de.harheimertc.test
|
||||
|
||||
// Intentionally empty
|
||||
@@ -0,0 +1,89 @@
|
||||
package de.harheimertc.test
|
||||
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.hilt.testing.TestInstallIn
|
||||
import de.harheimertc.data.ApiService
|
||||
import de.harheimertc.data.AuthStatusResponse
|
||||
import de.harheimertc.data.LoginRequest
|
||||
import de.harheimertc.data.LoginResponse
|
||||
import de.harheimertc.data.AuthUserDto
|
||||
import de.harheimertc.repositories.AuthRepository
|
||||
import de.harheimertc.data.SessionRefresher
|
||||
import dagger.hilt.InstallIn
|
||||
import retrofit2.Response
|
||||
import javax.inject.Singleton
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Proxy
|
||||
import de.harheimertc.repositories.LoginRepository
|
||||
import de.harheimertc.repositories.PasskeyRepository
|
||||
import de.harheimertc.repositories.AuthRepository as RepoAuthRepository
|
||||
|
||||
@Module
|
||||
@TestInstallIn(
|
||||
components = [SingletonComponent::class],
|
||||
replaces = [de.harheimertc.data.NetworkModule::class, de.harheimertc.di.RepositoryModule::class]
|
||||
)
|
||||
object TestHiltModules {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMoshi(): Moshi = Moshi.Builder().build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideApiService(): ApiService {
|
||||
val handler = InvocationHandler { _, method: Method, args: Array<Any>? ->
|
||||
when (method.name) {
|
||||
"login" -> Response.success(LoginResponse(success = true, accessToken = "test-token", refreshToken = "r", sessionId = "s", user = AuthUserDto(id = "1", email = "test@example.com", name = "Test")))
|
||||
"authStatus" -> Response.success(AuthStatusResponse(isLoggedIn = false))
|
||||
"publicNews" -> Response.success(de.harheimertc.data.NewsPublicResponse(news = listOf()))
|
||||
"memberNews" -> Response.success(de.harheimertc.data.NewsResponse(success = true, news = listOf()))
|
||||
else -> throw UnsupportedOperationException("ApiService method not implemented in test double: ${method.name}")
|
||||
}
|
||||
}
|
||||
|
||||
return Proxy.newProxyInstance(
|
||||
ApiService::class.java.classLoader,
|
||||
arrayOf(ApiService::class.java),
|
||||
handler,
|
||||
) as ApiService
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthRepository(): AuthRepository = object : AuthRepository {
|
||||
private var token: String? = "test-token"
|
||||
private var refresh: String? = "r"
|
||||
override fun getToken(): String? = token
|
||||
override fun getRefreshToken(): String? = refresh
|
||||
override fun getSessionId(): String? = "s"
|
||||
override fun setSession(accessToken: String?, refreshToken: String?, sessionId: String?) {
|
||||
token = accessToken
|
||||
refresh = refreshToken
|
||||
}
|
||||
|
||||
override fun clearSession() { token = null; refresh = null }
|
||||
override fun ensureDeviceKey(): String? = null
|
||||
override fun getDevicePublicKey(): String? = null
|
||||
override fun signWithDeviceKey(data: ByteArray): ByteArray? = null
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSessionRefresher(auth: AuthRepository, moshi: Moshi): SessionRefresher = SessionRefresher(auth, moshi)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideLoginRepository(api: ApiService, auth: AuthRepository, sessionRefresher: SessionRefresher): LoginRepository {
|
||||
return LoginRepository(api, auth, sessionRefresher)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePasskeyRepository(api: ApiService, auth: AuthRepository): PasskeyRepository {
|
||||
return PasskeyRepository(api, auth)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package de.harheimertc.ui
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
|
||||
class TestActivity : ComponentActivity()
|
||||
@@ -0,0 +1,113 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CmsActivateResendTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun activateAndResend_buttonsAreClickable() {
|
||||
composeTestRule.setContent {
|
||||
androidx.compose.material3.TextButton(onClick = { /* no-op */ }) { androidx.compose.material3.Text("Deaktivieren") }
|
||||
androidx.compose.material3.TextButton(onClick = { /* no-op */ }) { androidx.compose.material3.Text("Invite erneut") }
|
||||
}
|
||||
|
||||
// wait until nodes appear to avoid race conditions on slower devices
|
||||
fun waitForText(text: String, timeoutMs: Long = 15000L) {
|
||||
try {
|
||||
composeTestRule.waitUntil(timeoutMs) {
|
||||
try {
|
||||
composeTestRule.onAllNodes(hasText(text)).fetchSemanticsNodes().isNotEmpty()
|
||||
} catch (_: AssertionError) {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// dump semantics tree for debugging before failing
|
||||
try {
|
||||
composeTestRule.onRoot().printToLog("CmsActivateResendTest-SEMTREE")
|
||||
} catch (_: Throwable) { /* best-effort logging */ }
|
||||
throw AssertionError("Timed out waiting for text: '$text'")
|
||||
}
|
||||
}
|
||||
|
||||
// helper: find the nearest parent node that has a click action
|
||||
fun findClickableParent(text: String): SemanticsNodeInteraction {
|
||||
val all = composeTestRule.onAllNodes(hasText(text))
|
||||
if (all.fetchSemanticsNodes().isEmpty()) {
|
||||
try {
|
||||
composeTestRule.onRoot().printToLog("CmsActivateResendTest-SEMTREE-NOT-FOUND-$text")
|
||||
} catch (_: Throwable) { }
|
||||
throw AssertionError("No node found with text '$text'")
|
||||
}
|
||||
|
||||
// Log matches for debugging
|
||||
try {
|
||||
val matches = all.fetchSemanticsNodes()
|
||||
Log.d("CmsActivateResendTest", "Found ${matches.size} node(s) for text '$text'")
|
||||
matches.forEachIndexed { i, n -> Log.d("CmsActivateResendTest", "Match[$i]: ${n}") }
|
||||
} catch (_: Throwable) { /* ignore logging failures */ }
|
||||
|
||||
var node = try {
|
||||
// prefer the single-node API, but fall back to the first match if ambiguous
|
||||
composeTestRule.onNode(hasText(text))
|
||||
} catch (_: AssertionError) {
|
||||
all[0]
|
||||
}
|
||||
|
||||
// climb a few parents to find the clickable wrapper
|
||||
repeat(8) {
|
||||
try {
|
||||
node.assert(hasClickAction())
|
||||
try { Log.d("CmsActivateResendTest", "Clickable node found for '$text': ${node.fetchSemanticsNode()}") } catch (_: Throwable) {}
|
||||
return node
|
||||
} catch (_: AssertionError) {
|
||||
try { Log.d("CmsActivateResendTest", "Node not clickable yet, current node: ${node.fetchSemanticsNode()}") } catch (_: Throwable) {}
|
||||
node = node.onParent()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
composeTestRule.onRoot().printToLog("CmsActivateResendTest-SEMTREE-NO-CLICK-$text")
|
||||
} catch (_: Throwable) { }
|
||||
throw AssertionError("No clickable parent found for text '$text'")
|
||||
}
|
||||
|
||||
waitForText("Deaktivieren")
|
||||
val deactivateNode = findClickableParent("Deaktivieren")
|
||||
deactivateNode.assertExists()
|
||||
deactivateNode.assertIsDisplayed()
|
||||
deactivateNode.assert(hasClickAction())
|
||||
composeTestRule.waitForIdle()
|
||||
try {
|
||||
deactivateNode.performClick()
|
||||
} catch (e: Throwable) {
|
||||
composeTestRule.onRoot().printToLog("CmsActivateResendTest-CLICK-FAIL-DEACTIVATE")
|
||||
throw e
|
||||
}
|
||||
|
||||
waitForText("Invite erneut")
|
||||
val inviteNode = findClickableParent("Invite erneut")
|
||||
inviteNode.assertExists()
|
||||
inviteNode.assertIsDisplayed()
|
||||
inviteNode.assert(hasClickAction())
|
||||
composeTestRule.waitForIdle()
|
||||
try {
|
||||
inviteNode.performClick()
|
||||
} catch (e: Throwable) {
|
||||
composeTestRule.onRoot().printToLog("CmsActivateResendTest-CLICK-FAIL-INVITE")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.junit4.ComposeTestRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CmsRolesDialogTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
class FakeVm {
|
||||
var calledId: String? = null
|
||||
var calledRoles: List<String>? = null
|
||||
fun updateUserRoles(id: String, roles: List<String>) {
|
||||
calledId = id
|
||||
calledRoles = roles
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rolesDialog_callsUpdateUserRoles() {
|
||||
val fake = FakeVm()
|
||||
val initialRoles = listOf("admin")
|
||||
|
||||
composeTestRule.setContent {
|
||||
val show = remember { mutableStateOf(false) }
|
||||
val selected = remember { mutableStateListOf<String>().apply { addAll(initialRoles) } }
|
||||
Column {
|
||||
Button(onClick = { show.value = true }) { Text("Rollen") }
|
||||
if (show.value) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { show.value = false },
|
||||
title = { Text("Rollen bearbeiten") },
|
||||
text = {
|
||||
Column(modifier = Modifier.padding(4.dp)) {
|
||||
// simple checkbox row for admin only (representative)
|
||||
Row {
|
||||
Checkbox(checked = selected.contains("admin"), onCheckedChange = { checked ->
|
||||
if (checked) selected.add("admin") else selected.remove("admin")
|
||||
})
|
||||
Text("admin", modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
fake.updateUserRoles("42", selected.toList())
|
||||
show.value = false
|
||||
}) { Text("Speichern") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { show.value = false }) { Text("Abbrechen") } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open dialog
|
||||
composeTestRule.onNodeWithText("Rollen").performClick()
|
||||
// Save immediately (we keep admin preselected)
|
||||
composeTestRule.onNodeWithText("Speichern").performClick()
|
||||
|
||||
assert(fake.calledId == "42")
|
||||
assert(fake.calledRoles?.contains("admin") == true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.harheimertc.ui.screens.cms
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CmsScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun cmsScreen_placeholder() {
|
||||
composeTestRule.setContent {
|
||||
Text("CMS Placeholder")
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("CMS Placeholder").assertExists()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.harheimertc.ui.screens.gallery
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GalleryScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun galleryScreen_rendersPlaceholder() {
|
||||
composeTestRule.setContent {
|
||||
GalleryScreen()
|
||||
}
|
||||
|
||||
composeTestRule.onRoot().assertExists()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package de.harheimertc.ui.screens.home
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.*
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class HomeScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun homeScreen_renders() {
|
||||
composeTestRule.setContent {
|
||||
val navController = rememberNavController()
|
||||
HomeScreen(navController = navController, showNavigationHeader = false)
|
||||
}
|
||||
|
||||
composeTestRule.onRoot().assertExists()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package de.harheimertc.ui.screens.login
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class LoginScreenTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
@Test
|
||||
fun loginScreen_showsFields() {
|
||||
composeTestRule.setContent {
|
||||
val navController = rememberNavController()
|
||||
LoginScreen(navController = navController, showBackNavigation = false)
|
||||
}
|
||||
|
||||
composeTestRule.onNodeWithText("E-Mail-Adresse", useUnmergedTree = true).assertExists()
|
||||
composeTestRule.onNodeWithText("Passwort", useUnmergedTree = true).assertExists()
|
||||
composeTestRule.onNodeWithText("Anmelden", useUnmergedTree = true).assertExists()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user