feat: hydrate board label chip colors from API
This commit is contained in:
@@ -97,7 +97,7 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
|
||||
- Pressing "Delete" in the modal dialog MUST show a second confirmation modal asking if the user is sure, with buttons for "Cancel" and "I'm sure".
|
||||
- Only on pressing "I'm sure" in the second confirmation modal dialog should a board delete request be sent to the API.
|
||||
- Long-pressing any of the buttons must show a tooltip with the button name.
|
||||
- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session.
|
||||
- Current status: implemented in `BoardDetailActivity` with `ViewPager2` (one list per page), inline list-title edit, card rendering (title/tags/due date locale formatting and expiry color), cross-page card selection, page-scoped select-all, move dialog with list selector, two-step delete confirmation, mutation guards while in progress, and API-backed reload/reconciliation behavior through `BoardDetailViewModel` and `BoardDetailRepository`. Label chip border colors are hydrated from the Kan.bn `Get a label by public ID` endpoint (`colourCode`) and cached in-memory by `BoardDetailRepository` so each label color is fetched only once per app process. Selection action icons use local vector drawables (`ic_select_all_grid_24`, `ic_move_cards_horizontal_24`, `ic_delete_24`) with day/night resource variants so dark mode uses light icon fills automatically. Startup blocking dialogs are shown for missing board id and missing session.
|
||||
|
||||
**Card detail view**
|
||||
- The view shows the card's title in bold letters. Tapping on the card's title allows editing it.
|
||||
@@ -132,5 +132,5 @@ Kanbn4Droid is an unofficial app to connect to and manipulate data stored in sel
|
||||
- It is preferable to use standard Android libraries and tools whenever possible.
|
||||
- If a task would be better served by or can only be done with external libraries then it is allowed to use them but you MUST ask the user for permission to add the library to the project first.
|
||||
- The documentation for the Kan.bn API is available here https://docs.kan.bn/api-reference/introduction
|
||||
- Never commit or push code unless explicitely prompted to do so.
|
||||
- Never push code unless explicitely prompted to do so.
|
||||
- After every run, update this file as needed to reflect the changes made.
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.text.DateFormat
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Date
|
||||
@@ -148,6 +149,22 @@ class BoardDetailFlowTest {
|
||||
onView(withId(R.id.cardDueDateText)).check(matches(withCurrentTextColor(expectedColor ?: Color.BLACK)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invalidTagColorFallsBackToOnSurfaceColor() {
|
||||
defaultDataSource.currentDetail = detailWithInvalidTagColor()
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
var expectedColor: Int? = null
|
||||
scenario.onActivity { activity ->
|
||||
expectedColor = MaterialColors.getColor(
|
||||
activity.findViewById(android.R.id.content),
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
)
|
||||
}
|
||||
|
||||
onView(withText("Backend")).check(matches(withChipStrokeColor(expectedColor ?: Color.DKGRAY)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dueDateUsesSystemLocaleFormatting() {
|
||||
Locale.setDefault(Locale.FRANCE)
|
||||
@@ -571,6 +588,21 @@ class BoardDetailFlowTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun withChipStrokeColor(expectedColor: Int): Matcher<View> {
|
||||
return object : TypeSafeMatcher<View>() {
|
||||
override fun describeTo(description: Description) {
|
||||
description.appendText("with chip stroke color: $expectedColor")
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View): Boolean {
|
||||
if (item !is Chip) {
|
||||
return false
|
||||
}
|
||||
return item.chipStrokeColor?.defaultColor == expectedColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun awaitCondition(timeoutMs: Long = 4_000L, condition: () -> Boolean) {
|
||||
val instrumentation = androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()
|
||||
val start = System.currentTimeMillis()
|
||||
@@ -757,6 +789,25 @@ class BoardDetailFlowTest {
|
||||
)
|
||||
}
|
||||
|
||||
fun detailWithInvalidTagColor(): BoardDetail {
|
||||
return detailOneList().copy(
|
||||
lists = listOf(
|
||||
BoardListDetail(
|
||||
id = "list-1",
|
||||
title = "To Do",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "card-1",
|
||||
title = "Card 1",
|
||||
tags = listOf(BoardTagSummary("tag-1", "Backend", "bad-color")),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun detailWithCardTitle(title: String): BoardDetail {
|
||||
return detailOneList().copy(
|
||||
lists = listOf(
|
||||
|
||||
@@ -23,6 +23,7 @@ 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.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
||||
@@ -199,5 +200,13 @@ class BoardsFlowTest {
|
||||
boards.removeAll { it.id == boardId }
|
||||
return BoardsApiResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): BoardsApiResult<LabelDetail> {
|
||||
return BoardsApiResult.Failure("Not needed in boards flow tests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import space.hackenslacker.kanbn4droid.app.auth.AuthFailureReason
|
||||
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.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@@ -200,5 +201,13 @@ class LoginFlowTest {
|
||||
private val result: AuthResult,
|
||||
) : KanbnApiClient {
|
||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = result
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult<LabelDetail> {
|
||||
return space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult.Failure("Not needed in login tests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,8 +69,17 @@ interface KanbnApiClient {
|
||||
suspend fun deleteCard(baseUrl: String, apiKey: String, cardId: String): BoardsApiResult<Unit> {
|
||||
return BoardsApiResult.Failure("Card deletion is not implemented.")
|
||||
}
|
||||
|
||||
suspend fun getLabelByPublicId(baseUrl: String, apiKey: String, labelId: String): BoardsApiResult<LabelDetail> {
|
||||
return BoardsApiResult.Failure("Label detail is not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
data class LabelDetail(
|
||||
val id: String,
|
||||
val colorHex: String,
|
||||
)
|
||||
|
||||
class HttpKanbnApiClient : KanbnApiClient {
|
||||
|
||||
override suspend fun listWorkspaces(baseUrl: String, apiKey: String): BoardsApiResult<List<WorkspaceSummary>> {
|
||||
@@ -304,6 +313,29 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): BoardsApiResult<LabelDetail> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
request(
|
||||
baseUrl = baseUrl,
|
||||
path = "/api/v1/labels/$labelId",
|
||||
method = "GET",
|
||||
apiKey = apiKey,
|
||||
) { code, body ->
|
||||
if (code in 200..299) {
|
||||
parseLabelDetail(body, labelId)
|
||||
?.let { BoardsApiResult.Success(it) }
|
||||
?: BoardsApiResult.Failure("Malformed label detail response.")
|
||||
} else {
|
||||
BoardsApiResult.Failure(serverMessage(body, code))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> request(
|
||||
baseUrl: String,
|
||||
path: String,
|
||||
@@ -504,6 +536,23 @@ class HttpKanbnApiClient : KanbnApiClient {
|
||||
return BoardDetail(id = boardId, title = boardTitle, lists = lists)
|
||||
}
|
||||
|
||||
private fun parseLabelDetail(body: String, fallbackId: String): LabelDetail? {
|
||||
if (body.isBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val root = parseJsonObject(body) ?: return null
|
||||
val data = root["data"] as? Map<*, *>
|
||||
val label = (data?.get("label") as? Map<*, *>)
|
||||
?: data
|
||||
?: (root["label"] as? Map<*, *>)
|
||||
?: root
|
||||
|
||||
val id = extractId(label).ifBlank { fallbackId }
|
||||
val colorHex = extractString(label, "colourCode", "colorCode", "colorHex", "color", "hex")
|
||||
return LabelDetail(id = id, colorHex = colorHex)
|
||||
}
|
||||
|
||||
private fun parseLists(board: Map<*, *>): List<BoardListDetail> {
|
||||
return extractObjectArray(board, "lists", "items", "data").mapNotNull { rawList ->
|
||||
val id = extractId(rawList)
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||
import space.hackenslacker.kanbn4droid.app.auth.KanbnApiClient
|
||||
import space.hackenslacker.kanbn4droid.app.auth.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||
|
||||
@@ -14,6 +15,8 @@ class BoardDetailRepository(
|
||||
private val apiClient: KanbnApiClient,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
private val labelColorCache = mutableMapOf<String, String>()
|
||||
|
||||
companion object {
|
||||
const val MISSING_SESSION_MESSAGE = "Missing session. Please sign in again."
|
||||
}
|
||||
@@ -33,7 +36,13 @@ class BoardDetailRepository(
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
boardId = normalizedBoardId,
|
||||
)
|
||||
).mapSuccess { detail ->
|
||||
hydrateLabelColors(
|
||||
detail = detail,
|
||||
baseUrl = session.baseUrl,
|
||||
apiKey = session.apiKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun renameList(listId: String, newTitle: String): BoardsApiResult<Unit> {
|
||||
@@ -192,6 +201,63 @@ class BoardDetailRepository(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun hydrateLabelColors(
|
||||
detail: BoardDetail,
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
): BoardDetail {
|
||||
val missingColorIds = detail.lists
|
||||
.asSequence()
|
||||
.flatMap { list -> list.cards.asSequence() }
|
||||
.flatMap { card -> card.tags.asSequence() }
|
||||
.map { it.id.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.distinct()
|
||||
.filter { !labelColorCache.containsKey(it) }
|
||||
.toList()
|
||||
|
||||
missingColorIds.forEach { labelId ->
|
||||
val colorHex = when (val labelResult = apiClient.getLabelByPublicId(baseUrl, apiKey, labelId)) {
|
||||
is BoardsApiResult.Success -> normalizeColorHex(labelResult.value)
|
||||
is BoardsApiResult.Failure -> ""
|
||||
}
|
||||
if (colorHex.isNotBlank()) {
|
||||
labelColorCache[labelId] = colorHex
|
||||
}
|
||||
}
|
||||
|
||||
val hydratedLists = detail.lists.map { list ->
|
||||
list.copy(
|
||||
cards = list.cards.map { card ->
|
||||
card.copy(
|
||||
tags = card.tags.map { tag ->
|
||||
val cached = labelColorCache[tag.id.trim()].orEmpty()
|
||||
val merged = if (cached.isNotBlank()) cached else tag.colorHex
|
||||
if (merged == tag.colorHex) {
|
||||
tag
|
||||
} else {
|
||||
tag.copy(colorHex = merged)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return if (hydratedLists == detail.lists) detail else detail.copy(lists = hydratedLists)
|
||||
}
|
||||
|
||||
private fun normalizeColorHex(label: LabelDetail): String {
|
||||
return label.colorHex.trim()
|
||||
}
|
||||
|
||||
private suspend fun <T, R> BoardsApiResult<T>.mapSuccess(transform: suspend (T) -> R): BoardsApiResult<R> {
|
||||
return when (this) {
|
||||
is BoardsApiResult.Success -> BoardsApiResult.Success(transform(this.value))
|
||||
is BoardsApiResult.Failure -> BoardsApiResult.Failure(this.message)
|
||||
}
|
||||
}
|
||||
|
||||
private data class SessionSnapshot(
|
||||
val baseUrl: String,
|
||||
val apiKey: String,
|
||||
|
||||
@@ -341,6 +341,84 @@ class HttpKanbnApiClientBoardDetailParsingTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLabelByPublicIdParsesColourCodeFromWrappedPayload() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.register(
|
||||
path = "/api/v1/labels/label-1",
|
||||
status = 200,
|
||||
responseBody =
|
||||
"""
|
||||
{
|
||||
"data": {
|
||||
"label": {
|
||||
"public_id": "label-1",
|
||||
"colourCode": "#A1B2C3"
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-1")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail
|
||||
assertEquals("label-1", detail.id)
|
||||
assertEquals("#A1B2C3", detail.colorHex)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLabelByPublicIdUsesFallbackIdWhenPayloadHasNoId() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.register(
|
||||
path = "/api/v1/labels/label-fallback",
|
||||
status = 200,
|
||||
responseBody =
|
||||
"""
|
||||
{
|
||||
"label": {
|
||||
"colorCode": "#ABCDEF"
|
||||
}
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val result = HttpKanbnApiClient().getLabelByPublicId(server.baseUrl, "api-key", "label-fallback")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val detail = (result as BoardsApiResult.Success<*>).value as LabelDetail
|
||||
assertEquals("label-fallback", detail.id)
|
||||
assertEquals("#ABCDEF", detail.colorHex)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getLabelByPublicIdFailureUsesServerMessageAndFallback() = runTest {
|
||||
TestServer().use { server ->
|
||||
server.register(
|
||||
path = "/api/v1/labels/label-msg",
|
||||
status = 404,
|
||||
responseBody = """{"message":"Label not found"}""",
|
||||
)
|
||||
server.register(
|
||||
path = "/api/v1/labels/label-fallback",
|
||||
status = 500,
|
||||
responseBody = "{}",
|
||||
)
|
||||
|
||||
val client = HttpKanbnApiClient()
|
||||
val withMessage = client.getLabelByPublicId(server.baseUrl, "api", "label-msg")
|
||||
val fallback = client.getLabelByPublicId(server.baseUrl, "api", "label-fallback")
|
||||
|
||||
assertTrue(withMessage is BoardsApiResult.Failure)
|
||||
assertEquals("Label not found", (withMessage as BoardsApiResult.Failure).message)
|
||||
assertTrue(fallback is BoardsApiResult.Failure)
|
||||
assertEquals("Server error: 500", (fallback as BoardsApiResult.Failure).message)
|
||||
}
|
||||
}
|
||||
|
||||
private data class CapturedRequest(
|
||||
val method: String,
|
||||
val path: String,
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.Test
|
||||
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.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
||||
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
||||
@@ -99,6 +100,95 @@ class BoardDetailRepositoryTest {
|
||||
assertEquals("Server says no", (result as BoardsApiResult.Failure).message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoardDetailFetchesLabelColorCodesAndCachesThem() = runTest {
|
||||
val detailWithTags = BoardDetail(
|
||||
id = "board-1",
|
||||
title = "Board",
|
||||
lists = listOf(
|
||||
BoardListDetail(
|
||||
id = "list-1",
|
||||
title = "To Do",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "card-1",
|
||||
title = "Card 1",
|
||||
tags = listOf(
|
||||
BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""),
|
||||
BoardTagSummary(id = "label-2", name = "Backend", colorHex = "#000000"),
|
||||
),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
BoardCardSummary(
|
||||
id = "card-2",
|
||||
title = "Card 2",
|
||||
tags = listOf(
|
||||
BoardTagSummary(id = "label-1", name = "Urgent", colorHex = ""),
|
||||
),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val apiClient = FakeBoardDetailApiClient().apply {
|
||||
boardDetailResult = BoardsApiResult.Success(detailWithTags)
|
||||
labelByIdResults = mapOf(
|
||||
"label-1" to BoardsApiResult.Success(LabelDetail("label-1", "#112233")),
|
||||
"label-2" to BoardsApiResult.Success(LabelDetail("label-2", "#445566")),
|
||||
)
|
||||
}
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val first = repository.getBoardDetail("board-1")
|
||||
val second = repository.getBoardDetail("board-1")
|
||||
|
||||
assertTrue(first is BoardsApiResult.Success<*>)
|
||||
assertTrue(second is BoardsApiResult.Success<*>)
|
||||
val firstDetail = (first as BoardsApiResult.Success<BoardDetail>).value
|
||||
val secondDetail = (second as BoardsApiResult.Success<BoardDetail>).value
|
||||
assertEquals("#112233", firstDetail.lists[0].cards[0].tags[0].colorHex)
|
||||
assertEquals("#445566", firstDetail.lists[0].cards[0].tags[1].colorHex)
|
||||
assertEquals("#112233", firstDetail.lists[0].cards[1].tags[0].colorHex)
|
||||
assertEquals("#112233", secondDetail.lists[0].cards[0].tags[0].colorHex)
|
||||
assertEquals("#445566", secondDetail.lists[0].cards[0].tags[1].colorHex)
|
||||
assertEquals(listOf("label-1", "label-2"), apiClient.getLabelByPublicIdCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getBoardDetailKeepsOriginalColorWhenLabelLookupFails() = runTest {
|
||||
val detailWithTag = BoardDetail(
|
||||
id = "board-1",
|
||||
title = "Board",
|
||||
lists = listOf(
|
||||
BoardListDetail(
|
||||
id = "list-1",
|
||||
title = "To Do",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "card-1",
|
||||
title = "Card 1",
|
||||
tags = listOf(BoardTagSummary(id = "label-1", name = "Urgent", colorHex = "#ABCDEF")),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
val apiClient = FakeBoardDetailApiClient().apply {
|
||||
boardDetailResult = BoardsApiResult.Success(detailWithTag)
|
||||
labelByIdResults = mapOf("label-1" to BoardsApiResult.Failure("Server unavailable"))
|
||||
}
|
||||
val repository = createRepository(apiClient = apiClient)
|
||||
|
||||
val result = repository.getBoardDetail("board-1")
|
||||
|
||||
assertTrue(result is BoardsApiResult.Success<*>)
|
||||
val detail = (result as BoardsApiResult.Success<BoardDetail>).value
|
||||
assertEquals("#ABCDEF", detail.lists[0].cards[0].tags[0].colorHex)
|
||||
assertEquals(listOf("label-1"), apiClient.getLabelByPublicIdCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun renameListPropagatesApiFailureMessageUnchanged() = runTest {
|
||||
val apiClient = FakeBoardDetailApiClient().apply {
|
||||
@@ -366,6 +456,7 @@ class BoardDetailRepositoryTest {
|
||||
var renameListResult: BoardsApiResult<Unit> = BoardsApiResult.Success(Unit)
|
||||
var moveOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||
var deleteOutcomes: Map<String, BoardsApiResult<Unit>> = emptyMap()
|
||||
var labelByIdResults: Map<String, BoardsApiResult<LabelDetail>> = emptyMap()
|
||||
|
||||
var listWorkspacesCalls: Int = 0
|
||||
var lastBoardId: String? = null
|
||||
@@ -374,6 +465,7 @@ class BoardDetailRepositoryTest {
|
||||
var movedCardIds: MutableList<String> = mutableListOf()
|
||||
var deletedCardIds: MutableList<String> = mutableListOf()
|
||||
var lastMoveTargetListId: String? = null
|
||||
var getLabelByPublicIdCalls: MutableList<String> = mutableListOf()
|
||||
|
||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||
|
||||
@@ -425,6 +517,15 @@ class BoardDetailRepositoryTest {
|
||||
return deleteOutcomes[cardId] ?: BoardsApiResult.Success(Unit)
|
||||
}
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): BoardsApiResult<LabelDetail> {
|
||||
getLabelByPublicIdCalls += labelId
|
||||
return labelByIdResults[labelId] ?: BoardsApiResult.Failure("Missing fake label")
|
||||
}
|
||||
|
||||
override suspend fun listBoards(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.junit.Test
|
||||
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.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
|
||||
class BoardsRepositoryTest {
|
||||
@@ -250,5 +251,13 @@ class BoardsRepositoryTest {
|
||||
lastDeletedId = boardId
|
||||
return deleteBoardResult
|
||||
}
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): BoardsApiResult<LabelDetail> {
|
||||
return BoardsApiResult.Failure("Not needed in boards tests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.junit.Test
|
||||
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.LabelDetail
|
||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -194,5 +195,13 @@ class BoardsViewModelTest {
|
||||
lastDeletedId = boardId
|
||||
return deleteBoardResult
|
||||
}
|
||||
|
||||
override suspend fun getLabelByPublicId(
|
||||
baseUrl: String,
|
||||
apiKey: String,
|
||||
labelId: String,
|
||||
): BoardsApiResult<LabelDetail> {
|
||||
return BoardsApiResult.Failure("Not needed in boards tests")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user