fix: finalize board detail action icons and startup guards

This commit is contained in:
2026-03-16 03:15:31 -04:00
parent e72e584fd4
commit 81cd654611
8 changed files with 184 additions and 10 deletions

View File

@@ -5,6 +5,7 @@ import android.graphics.Color
import android.view.inputmethod.EditorInfo
import android.view.View
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
@@ -17,7 +18,6 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.withId
@@ -38,6 +38,7 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -48,6 +49,8 @@ import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailDataSource
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardListDetail
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardTagSummary
import space.hackenslacker.kanbn4droid.app.boarddetail.CardBatchMutationResult
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
@RunWith(AndroidJUnit4::class)
@@ -59,6 +62,9 @@ class BoardDetailFlowTest {
@Before
fun setUp() {
MainActivity.dependencies.clear()
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore("https://kan.bn/") }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore("api") }
originalLocale = Locale.getDefault()
Intents.init()
defaultDataSource = FakeBoardDetailDataSource(initialDetail = detailOneList())
@@ -71,6 +77,7 @@ class BoardDetailFlowTest {
Intents.release()
BoardDetailActivity.testDataSourceFactory = null
BoardDetailActivity.testUiStateObserver = null
MainActivity.dependencies.clear()
originalLocale?.let { Locale.setDefault(it) }
}
@@ -256,6 +263,59 @@ class BoardDetailFlowTest {
}
}
@Test
fun selectionActionIconsMatchExpectedResources() {
val scenario = launchBoardDetail()
onView(withText("Card 1")).perform(longClick())
scenario.onActivity { activity ->
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
assertNotNull(menu.findItem(R.id.actionSelectAll)?.icon)
assertNotNull(menu.findItem(R.id.actionMoveCards)?.icon)
assertNotNull(menu.findItem(R.id.actionDeleteCards)?.icon)
assertEquals(
AppCompatResources.getDrawable(activity, R.drawable.ic_select_all_grid_24)?.constantState,
menu.findItem(R.id.actionSelectAll)?.icon?.constantState,
)
assertEquals(
AppCompatResources.getDrawable(activity, R.drawable.ic_move_cards_horizontal_24)?.constantState,
menu.findItem(R.id.actionMoveCards)?.icon?.constantState,
)
assertEquals(
AppCompatResources.getDrawable(activity, R.drawable.ic_delete_24)?.constantState,
menu.findItem(R.id.actionDeleteCards)?.icon?.constantState,
)
}
}
@Test
fun missingBoardIdShowsBlockingDialogAndFinishes() {
val scenario = launchBoardDetail(boardId = null)
onView(withText(R.string.board_detail_unable_to_open_board)).inRoot(isDialog()).check(matches(isDisplayed()))
onView(withText(R.string.ok)).inRoot(isDialog()).perform(click())
scenario.onActivity { activity ->
assertTrue(activity.isFinishing)
}
}
@Test
fun missingSessionShowsBlockingDialogAndFinishes() {
BoardDetailActivity.testDataSourceFactory = null
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore(null) }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore(null) }
val scenario = launchBoardDetail()
onView(withText(R.string.board_detail_session_expired)).inRoot(isDialog()).check(matches(isDisplayed()))
onView(withText(R.string.ok)).inRoot(isDialog()).perform(click())
scenario.onActivity { activity ->
assertTrue(activity.isFinishing)
}
}
@Test
fun moveDialogShowsListSelector() {
defaultDataSource.currentDetail = detailTwoLists()
@@ -341,6 +401,7 @@ class BoardDetailFlowTest {
defaultDataSource.moveCalls == 1 && observedStates.lastOrNull()?.isMutating == false
}
onView(withText("Some cards could not be moved. Please try again.")).check(matches(isDisplayed()))
val last = observedStates.last()
assertEquals(setOf("card-2"), last.selectedCardIds)
}
@@ -360,6 +421,7 @@ class BoardDetailFlowTest {
defaultDataSource.moveCalls == 1 && observedStates.lastOrNull()?.isMutating == false
}
onView(withText("Move failed")).check(matches(isDisplayed()))
val last = observedStates.last()
assertEquals(setOf("card-1"), last.selectedCardIds)
}
@@ -412,6 +474,7 @@ class BoardDetailFlowTest {
defaultDataSource.deleteCalls == 1 && observedStates.lastOrNull()?.isMutating == false
}
onView(withText("Some cards could not be deleted. Please try again.")).check(matches(isDisplayed()))
val last = observedStates.last()
assertEquals(setOf("card-2"), last.selectedCardIds)
}
@@ -430,6 +493,7 @@ class BoardDetailFlowTest {
defaultDataSource.deleteCalls == 1 && observedStates.lastOrNull()?.isMutating == false
}
onView(withText("Delete failed")).check(matches(isDisplayed()))
val last = observedStates.last()
assertEquals(setOf("card-1"), last.selectedCardIds)
}
@@ -480,12 +544,15 @@ class BoardDetailFlowTest {
Intents.intended(hasExtra(CardDetailPlaceholderActivity.EXTRA_CARD_TITLE, expectedFallback))
}
private fun launchBoardDetail(): ActivityScenario<BoardDetailActivity> {
private fun launchBoardDetail(boardId: String? = "board-1"): ActivityScenario<BoardDetailActivity> {
val intent = Intent(
androidx.test.core.app.ApplicationProvider.getApplicationContext(),
BoardDetailActivity::class.java,
).putExtra(BoardDetailActivity.EXTRA_BOARD_ID, "board-1")
.putExtra(BoardDetailActivity.EXTRA_BOARD_TITLE, "Board")
)
if (boardId != null) {
intent.putExtra(BoardDetailActivity.EXTRA_BOARD_ID, boardId)
}
intent.putExtra(BoardDetailActivity.EXTRA_BOARD_TITLE, "Board")
return ActivityScenario.launch(intent)
}
@@ -611,6 +678,44 @@ class BoardDetailFlowTest {
}
}
private class InMemorySessionStore(
private var baseUrl: String?,
) : SessionStore {
override fun getBaseUrl(): String? = baseUrl
override fun saveBaseUrl(url: String) {
baseUrl = url
}
override fun clearBaseUrl() {
baseUrl = null
}
override fun getWorkspaceId(): String? = "ws-1"
override fun saveWorkspaceId(workspaceId: String) {
}
override fun clearWorkspaceId() {
}
}
private class InMemoryApiKeyStore(
private var key: String?,
) : ApiKeyStore {
override suspend fun saveApiKey(baseUrl: String, apiKey: String): Result<Unit> {
key = apiKey
return Result.success(Unit)
}
override suspend fun getApiKey(baseUrl: String): Result<String?> = Result.success(key)
override suspend fun invalidateApiKey(baseUrl: String): Result<Unit> {
key = null
return Result.success(Unit)
}
}
private companion object {
fun detailOneList(): BoardDetail {
return BoardDetail(