feat: implement boards list view workflows

This commit is contained in:
2026-03-15 20:44:07 -04:00
parent 8b8989a839
commit 30f9ac6b98
23 changed files with 1573 additions and 42 deletions

View File

@@ -0,0 +1,181 @@
package space.hackenslacker.kanbn4droid.app
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.intent.Intents
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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
import space.hackenslacker.kanbn4droid.app.auth.AuthResult
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
@RunWith(AndroidJUnit4::class)
class BoardsFlowTest {
@Before
fun setUp() {
MainActivity.dependencies.clear()
Intents.init()
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore("https://kan.bn/") }
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore("api") }
}
@After
fun tearDown() {
Intents.release()
MainActivity.dependencies.clear()
}
@Test
fun boardTapNavigatesToDetailPlaceholderWithExtras() {
MainActivity.dependencies.apiClientFactory = {
FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha")),
templates = listOf(BoardTemplate("tpl-1", "Starter")),
)
}
ActivityScenario.launch(BoardsActivity::class.java)
onView(withText("Alpha")).perform(click())
Intents.intended(hasComponent(BoardDetailPlaceholderActivity::class.java.name))
Intents.intended(hasExtra(BoardDetailPlaceholderActivity.EXTRA_BOARD_ID, "1"))
Intents.intended(hasExtra(BoardDetailPlaceholderActivity.EXTRA_BOARD_TITLE, "Alpha"))
}
@Test
fun createBoardWithTemplateNavigatesToCreatedBoard() {
MainActivity.dependencies.apiClientFactory = {
FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha")),
templates = listOf(BoardTemplate("tpl-1", "Starter")),
)
}
ActivityScenario.launch(BoardsActivity::class.java)
onView(withId(R.id.createBoardFab)).perform(click())
onView(withId(R.id.createBoardNameInput)).perform(replaceText("Roadmap"))
onView(withId(R.id.useTemplateChip)).perform(click())
onView(withId(android.R.id.button1)).inRoot(isDialog()).perform(click())
Intents.intended(hasComponent(BoardDetailPlaceholderActivity::class.java.name))
Intents.intended(hasExtra(BoardDetailPlaceholderActivity.EXTRA_BOARD_TITLE, "Roadmap"))
}
@Test
fun deleteBoardRequiresSecondConfirmation() {
val fake = FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha"), BoardSummary("2", "Beta")),
templates = emptyList(),
)
MainActivity.dependencies.apiClientFactory = { fake }
ActivityScenario.launch(BoardsActivity::class.java)
onView(withText("Alpha")).perform(longClick())
onView(withText(R.string.delete)).inRoot(isDialog()).perform(click())
onView(withText(R.string.im_sure)).inRoot(isDialog()).perform(click())
onView(withText("Alpha")).check(doesNotExist())
onView(withText("Beta")).check(matches(isDisplayed()))
}
@Test
fun pullToRefreshWorks() {
MainActivity.dependencies.apiClientFactory = {
FakeBoardsApiClient(
boards = mutableListOf(BoardSummary("1", "Alpha")),
templates = emptyList(),
)
}
ActivityScenario.launch(BoardsActivity::class.java)
onView(withId(R.id.boardsSwipeRefresh)).perform(swipeDown())
onView(withText("Alpha")).check(matches(isDisplayed()))
}
private class InMemorySessionStore(
private var baseUrl: String? = null,
) : SessionStore {
override fun getBaseUrl(): String? = baseUrl
override fun saveBaseUrl(url: String) {
baseUrl = url
}
override fun clearBaseUrl() {
baseUrl = null
}
}
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 class FakeBoardsApiClient(
private val boards: MutableList<BoardSummary>,
private val templates: List<BoardTemplate>,
) : KanbnApiClient {
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
override suspend fun listBoards(baseUrl: String, apiKey: String): BoardsApiResult<List<BoardSummary>> {
return BoardsApiResult.Success(boards.toList())
}
override suspend fun listBoardTemplates(
baseUrl: String,
apiKey: String,
): BoardsApiResult<List<BoardTemplate>> {
return BoardsApiResult.Success(templates)
}
override suspend fun createBoard(
baseUrl: String,
apiKey: String,
name: String,
templateId: String?,
): BoardsApiResult<BoardSummary> {
val next = BoardSummary((boards.size + 1).toString(), name)
boards.add(next)
return BoardsApiResult.Success(next)
}
override suspend fun deleteBoard(baseUrl: String, apiKey: String, boardId: String): BoardsApiResult<Unit> {
boards.removeAll { it.id == boardId }
return BoardsApiResult.Success(Unit)
}
}
}

View File

@@ -59,7 +59,7 @@ class LoginFlowTest {
ActivityScenario.launch(MainActivity::class.java)
Intents.intended(hasComponent(BoardsPlaceholderActivity::class.java.name))
Intents.intended(hasComponent(BoardsActivity::class.java.name))
}
@Test
@@ -91,7 +91,7 @@ class LoginFlowTest {
onView(withId(R.id.apiKeyInput)).perform(replaceText("kan_new"), closeSoftKeyboard())
onView(withId(R.id.signInButton)).perform(click())
Intents.intended(hasComponent(BoardsPlaceholderActivity::class.java.name))
Intents.intended(hasComponent(BoardsActivity::class.java.name))
assertEquals("https://kan.bn/", sessionStore.getBaseUrl())
assertEquals("kan_new", keyStore.savedKey)
}