feat: integrate boards drawer interactions and workspace switching
This commit is contained in:
@@ -2,7 +2,12 @@ package space.hackenslacker.kanbn4droid.app
|
|||||||
|
|
||||||
import androidx.test.core.app.ActivityScenario
|
import androidx.test.core.app.ActivityScenario
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
import androidx.test.espresso.contrib.DrawerActions
|
import androidx.test.espresso.contrib.DrawerActions
|
||||||
|
import androidx.test.espresso.action.CoordinatesProvider
|
||||||
|
import androidx.test.espresso.action.GeneralSwipeAction
|
||||||
|
import androidx.test.espresso.action.Press
|
||||||
|
import androidx.test.espresso.action.Swipe
|
||||||
import androidx.test.espresso.action.ViewActions.click
|
import androidx.test.espresso.action.ViewActions.click
|
||||||
import androidx.test.espresso.action.ViewActions.longClick
|
import androidx.test.espresso.action.ViewActions.longClick
|
||||||
import androidx.test.espresso.action.ViewActions.replaceText
|
import androidx.test.espresso.action.ViewActions.replaceText
|
||||||
@@ -17,7 +22,13 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
|||||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -29,7 +40,11 @@ import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
|||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
import space.hackenslacker.kanbn4droid.app.boards.BoardTemplate
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsApiResult
|
||||||
|
import space.hackenslacker.kanbn4droid.app.boards.DrawerProfile
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.WorkspaceSummary
|
import space.hackenslacker.kanbn4droid.app.boards.WorkspaceSummary
|
||||||
|
import java.util.ArrayDeque
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class BoardsFlowTest {
|
class BoardsFlowTest {
|
||||||
@@ -137,8 +152,153 @@ class BoardsFlowTest {
|
|||||||
onView(withId(R.id.drawerLogoutButton)).check(matches(isDisplayed()))
|
onView(withId(R.id.drawerLogoutButton)).check(matches(isDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun workspaceSelectionHighlightsAndRefreshesBoards() {
|
||||||
|
val fake = MultiWorkspaceFakeBoardsApiClient(
|
||||||
|
boardsByWorkspace = mapOf(
|
||||||
|
"ws-1" to listOf(BoardSummary("1", "Alpha")),
|
||||||
|
"ws-2" to listOf(BoardSummary("2", "Beta")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
MainActivity.dependencies.apiClientFactory = { fake }
|
||||||
|
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1") }
|
||||||
|
|
||||||
|
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
||||||
|
onView(withText("Platform")).perform(click())
|
||||||
|
|
||||||
|
onView(withText("Beta")).check(matches(isDisplayed()))
|
||||||
|
onView(withText("Alpha")).check(doesNotExist())
|
||||||
|
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val recycler = activity.findViewById<RecyclerView>(R.id.drawerWorkspacesRecyclerView)
|
||||||
|
val hasActivatedPlatform = (0 until recycler.childCount)
|
||||||
|
.map { recycler.getChildAt(it) }
|
||||||
|
.any { row ->
|
||||||
|
val title = row.findViewById<TextView>(R.id.workspaceTitleText).text.toString()
|
||||||
|
title == "Platform" && row.isActivated
|
||||||
|
}
|
||||||
|
assertTrue(hasActivatedPlatform)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertTrue(fake.listBoardsWorkspaceCalls.contains("ws-2"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun drawerOpensFromLeftEdgeGesture() {
|
||||||
|
MainActivity.dependencies.apiClientFactory = {
|
||||||
|
FakeBoardsApiClient(
|
||||||
|
boards = mutableListOf(BoardSummary("1", "Alpha")),
|
||||||
|
templates = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
onView(withId(R.id.boardsDrawerLayout)).perform(swipeFromLeftEdge())
|
||||||
|
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val drawer = activity.findViewById<DrawerLayout>(R.id.boardsDrawerLayout)
|
||||||
|
assertTrue(drawer.isDrawerOpen(GravityCompat.START))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun drawerRetryButtonReloadsAfterRecoverableFailure() {
|
||||||
|
val fake = MultiWorkspaceFakeBoardsApiClient(
|
||||||
|
boardsByWorkspace = mapOf("ws-1" to listOf(BoardSummary("1", "Alpha"))),
|
||||||
|
usersMeResponses = ArrayDeque(
|
||||||
|
listOf(
|
||||||
|
BoardsApiResult.Failure("Cannot reach server. Check your connection and URL."),
|
||||||
|
BoardsApiResult.Success(DrawerProfile(displayName = "Alice", email = "alice@example.com")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
workspaceResponses = ArrayDeque(
|
||||||
|
listOf(
|
||||||
|
BoardsApiResult.Failure("Cannot reach server. Check your connection and URL."),
|
||||||
|
BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main"), WorkspaceSummary("ws-2", "Platform"))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
MainActivity.dependencies.apiClientFactory = { fake }
|
||||||
|
|
||||||
|
ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
||||||
|
onView(withId(R.id.drawerRetryButton)).check(matches(isDisplayed())).perform(click())
|
||||||
|
onView(withText("Main")).check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
assertTrue(fake.listWorkspacesCalls >= 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun drawerUnauthorizedForcesSignOutToLogin() {
|
||||||
|
val sessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1")
|
||||||
|
val fake = MultiWorkspaceFakeBoardsApiClient(
|
||||||
|
boardsByWorkspace = mapOf("ws-1" to listOf(BoardSummary("1", "Alpha"))),
|
||||||
|
usersMeResponses = ArrayDeque(listOf(BoardsApiResult.Failure("Server error: 401"))),
|
||||||
|
workspaceResponses = ArrayDeque(listOf(BoardsApiResult.Failure("Server error: 401"))),
|
||||||
|
)
|
||||||
|
MainActivity.dependencies.sessionStoreFactory = { sessionStore }
|
||||||
|
MainActivity.dependencies.apiClientFactory = { fake }
|
||||||
|
|
||||||
|
ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
Intents.intended(hasComponent(MainActivity::class.java.name))
|
||||||
|
assertEquals(null, sessionStore.getWorkspaceId())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun workspaceSelectionClosesDrawerAfterSuccess() {
|
||||||
|
val fake = MultiWorkspaceFakeBoardsApiClient(
|
||||||
|
boardsByWorkspace = mapOf(
|
||||||
|
"ws-1" to listOf(BoardSummary("1", "Alpha")),
|
||||||
|
"ws-2" to listOf(BoardSummary("2", "Beta")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
MainActivity.dependencies.apiClientFactory = { fake }
|
||||||
|
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1") }
|
||||||
|
|
||||||
|
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
onView(withId(R.id.boardsDrawerLayout)).perform(DrawerActions.open())
|
||||||
|
onView(withText("Platform")).perform(click())
|
||||||
|
onView(withText("Beta")).check(matches(isDisplayed()))
|
||||||
|
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val drawer = activity.findViewById<DrawerLayout>(R.id.boardsDrawerLayout)
|
||||||
|
assertFalse(drawer.isDrawerOpen(GravityCompat.START))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun drawerWidthNeverExceedsOneThirdOfScreen() {
|
||||||
|
MainActivity.dependencies.apiClientFactory = {
|
||||||
|
FakeBoardsApiClient(
|
||||||
|
boards = mutableListOf(BoardSummary("1", "Alpha")),
|
||||||
|
templates = emptyList(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scenario = ActivityScenario.launch(BoardsActivity::class.java)
|
||||||
|
|
||||||
|
scenario.onActivity { activity ->
|
||||||
|
val drawerContent = activity.findViewById<View>(R.id.boardsDrawerContent)
|
||||||
|
val displayWidthPx = activity.resources.displayMetrics.widthPixels
|
||||||
|
assertTrue(drawerContent.layoutParams.width <= displayWidthPx / 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun swipeFromLeftEdge(): ViewAction {
|
||||||
|
val start = CoordinatesProvider { view -> floatArrayOf(5f, view.height * 0.5f) }
|
||||||
|
val end = CoordinatesProvider { view -> floatArrayOf(view.width * 0.6f, view.height * 0.5f) }
|
||||||
|
return GeneralSwipeAction(Swipe.FAST, start, end, Press.FINGER)
|
||||||
|
}
|
||||||
|
|
||||||
private class InMemorySessionStore(
|
private class InMemorySessionStore(
|
||||||
private var baseUrl: String? = null,
|
private var baseUrl: String? = null,
|
||||||
|
private var workspaceId: String? = "ws-1",
|
||||||
) : SessionStore {
|
) : SessionStore {
|
||||||
override fun getBaseUrl(): String? = baseUrl
|
override fun getBaseUrl(): String? = baseUrl
|
||||||
|
|
||||||
@@ -150,12 +310,14 @@ class BoardsFlowTest {
|
|||||||
baseUrl = null
|
baseUrl = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getWorkspaceId(): String? = "ws-1"
|
override fun getWorkspaceId(): String? = workspaceId
|
||||||
|
|
||||||
override fun saveWorkspaceId(workspaceId: String) {
|
override fun saveWorkspaceId(workspaceId: String) {
|
||||||
|
this.workspaceId = workspaceId
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearWorkspaceId() {
|
override fun clearWorkspaceId() {
|
||||||
|
workspaceId = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,14 +340,23 @@ class BoardsFlowTest {
|
|||||||
private class FakeBoardsApiClient(
|
private class FakeBoardsApiClient(
|
||||||
private val boards: MutableList<BoardSummary>,
|
private val boards: MutableList<BoardSummary>,
|
||||||
private val templates: List<BoardTemplate>,
|
private val templates: List<BoardTemplate>,
|
||||||
|
private val profile: DrawerProfile = DrawerProfile("Alice", "alice@example.com"),
|
||||||
|
private val workspaces: List<WorkspaceSummary> = listOf(
|
||||||
|
WorkspaceSummary("ws-1", "Main"),
|
||||||
|
WorkspaceSummary("ws-2", "Platform"),
|
||||||
|
),
|
||||||
) : KanbnApiClient {
|
) : KanbnApiClient {
|
||||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||||
|
|
||||||
|
override suspend fun getCurrentUser(baseUrl: String, apiKey: String): BoardsApiResult<DrawerProfile> {
|
||||||
|
return BoardsApiResult.Success(profile)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun listWorkspaces(
|
override suspend fun listWorkspaces(
|
||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
): BoardsApiResult<List<WorkspaceSummary>> {
|
): BoardsApiResult<List<WorkspaceSummary>> {
|
||||||
return BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
return BoardsApiResult.Success(workspaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun listBoards(
|
override suspend fun listBoards(
|
||||||
@@ -229,4 +400,90 @@ class BoardsFlowTest {
|
|||||||
return BoardsApiResult.Failure("Not needed in boards flow tests")
|
return BoardsApiResult.Failure("Not needed in boards flow tests")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MultiWorkspaceFakeBoardsApiClient(
|
||||||
|
private val boardsByWorkspace: Map<String, List<BoardSummary>>,
|
||||||
|
private val usersMeResponses: ArrayDeque<BoardsApiResult<DrawerProfile>> = ArrayDeque(
|
||||||
|
listOf(BoardsApiResult.Success(DrawerProfile("Alice", "alice@example.com"))),
|
||||||
|
),
|
||||||
|
private val workspaceResponses: ArrayDeque<BoardsApiResult<List<WorkspaceSummary>>> = ArrayDeque(
|
||||||
|
listOf(
|
||||||
|
BoardsApiResult.Success(
|
||||||
|
listOf(
|
||||||
|
WorkspaceSummary("ws-1", "Main"),
|
||||||
|
WorkspaceSummary("ws-2", "Platform"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) : KanbnApiClient {
|
||||||
|
var listWorkspacesCalls: Int = 0
|
||||||
|
val listBoardsWorkspaceCalls = mutableListOf<String>()
|
||||||
|
|
||||||
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||||
|
|
||||||
|
override suspend fun getCurrentUser(baseUrl: String, apiKey: String): BoardsApiResult<DrawerProfile> {
|
||||||
|
return if (usersMeResponses.isEmpty()) {
|
||||||
|
BoardsApiResult.Success(DrawerProfile("Alice", "alice@example.com"))
|
||||||
|
} else {
|
||||||
|
usersMeResponses.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listWorkspaces(
|
||||||
|
baseUrl: String,
|
||||||
|
apiKey: String,
|
||||||
|
): BoardsApiResult<List<WorkspaceSummary>> {
|
||||||
|
listWorkspacesCalls += 1
|
||||||
|
return if (workspaceResponses.isEmpty()) {
|
||||||
|
BoardsApiResult.Success(
|
||||||
|
listOf(
|
||||||
|
WorkspaceSummary("ws-1", "Main"),
|
||||||
|
WorkspaceSummary("ws-2", "Platform"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
workspaceResponses.removeFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listBoards(
|
||||||
|
baseUrl: String,
|
||||||
|
apiKey: String,
|
||||||
|
workspaceId: String,
|
||||||
|
): BoardsApiResult<List<BoardSummary>> {
|
||||||
|
listBoardsWorkspaceCalls += workspaceId
|
||||||
|
return BoardsApiResult.Success(boardsByWorkspace[workspaceId].orEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listBoardTemplates(
|
||||||
|
baseUrl: String,
|
||||||
|
apiKey: String,
|
||||||
|
workspaceId: String,
|
||||||
|
): BoardsApiResult<List<BoardTemplate>> {
|
||||||
|
return BoardsApiResult.Success(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun createBoard(
|
||||||
|
baseUrl: String,
|
||||||
|
apiKey: String,
|
||||||
|
workspaceId: String,
|
||||||
|
name: String,
|
||||||
|
templateId: String?,
|
||||||
|
): BoardsApiResult<BoardSummary> {
|
||||||
|
return BoardsApiResult.Success(BoardSummary("new", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteBoard(baseUrl: String, apiKey: String, boardId: String): BoardsApiResult<Unit> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package space.hackenslacker.kanbn4droid.app
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ArrayAdapter
|
import android.widget.ArrayAdapter
|
||||||
@@ -12,15 +13,19 @@ import android.widget.TextView
|
|||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.GravityCompat
|
||||||
|
import androidx.drawerlayout.widget.DrawerLayout
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.chip.Chip
|
import com.google.android.material.chip.Chip
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
import space.hackenslacker.kanbn4droid.app.auth.ApiKeyStore
|
||||||
@@ -31,10 +36,12 @@ import space.hackenslacker.kanbn4droid.app.auth.SessionPreferences
|
|||||||
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
import space.hackenslacker.kanbn4droid.app.auth.SessionStore
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
import space.hackenslacker.kanbn4droid.app.boards.BoardSummary
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsAdapter
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsAdapter
|
||||||
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsDrawerAdapter
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsRepository
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsRepository
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsUiEvent
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsUiEvent
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsUiState
|
||||||
import space.hackenslacker.kanbn4droid.app.boards.BoardsViewModel
|
import space.hackenslacker.kanbn4droid.app.boards.BoardsViewModel
|
||||||
|
import space.hackenslacker.kanbn4droid.app.boards.DrawerDataErrorCode
|
||||||
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity
|
import space.hackenslacker.kanbn4droid.app.boarddetail.BoardDetailActivity
|
||||||
|
|
||||||
class BoardsActivity : AppCompatActivity() {
|
class BoardsActivity : AppCompatActivity() {
|
||||||
@@ -47,8 +54,24 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
private lateinit var emptyStateText: TextView
|
private lateinit var emptyStateText: TextView
|
||||||
private lateinit var initialProgress: ProgressBar
|
private lateinit var initialProgress: ProgressBar
|
||||||
private lateinit var createFab: FloatingActionButton
|
private lateinit var createFab: FloatingActionButton
|
||||||
|
private lateinit var toolbar: MaterialToolbar
|
||||||
|
private lateinit var drawerLayout: DrawerLayout
|
||||||
|
private lateinit var drawerContent: View
|
||||||
|
private lateinit var drawerUsernameText: TextView
|
||||||
|
private lateinit var drawerEmailText: TextView
|
||||||
|
private lateinit var drawerLoadingIndicator: ProgressBar
|
||||||
|
private lateinit var drawerErrorText: TextView
|
||||||
|
private lateinit var drawerRetryButton: Button
|
||||||
|
private lateinit var drawerSettingsButton: Button
|
||||||
|
private lateinit var drawerLogoutButton: Button
|
||||||
|
private lateinit var drawerWorkspacesRecyclerView: RecyclerView
|
||||||
|
|
||||||
private lateinit var boardsAdapter: BoardsAdapter
|
private lateinit var boardsAdapter: BoardsAdapter
|
||||||
|
private lateinit var drawerAdapter: BoardsDrawerAdapter
|
||||||
|
|
||||||
|
private var hasRequestedInitialDrawerLoad = false
|
||||||
|
private var lastDrawerSwitchInFlight = false
|
||||||
|
private var pendingWorkspaceSelectionId: String? = null
|
||||||
|
|
||||||
private val viewModel: BoardsViewModel by viewModels {
|
private val viewModel: BoardsViewModel by viewModels {
|
||||||
BoardsViewModel.Factory(
|
BoardsViewModel.Factory(
|
||||||
@@ -82,11 +105,24 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun bindViews() {
|
private fun bindViews() {
|
||||||
|
drawerLayout = findViewById(R.id.boardsDrawerLayout)
|
||||||
|
drawerContent = findViewById(R.id.boardsDrawerContent)
|
||||||
|
toolbar = findViewById(R.id.boardsToolbar)
|
||||||
swipeRefresh = findViewById(R.id.boardsSwipeRefresh)
|
swipeRefresh = findViewById(R.id.boardsSwipeRefresh)
|
||||||
recyclerView = findViewById(R.id.boardsRecyclerView)
|
recyclerView = findViewById(R.id.boardsRecyclerView)
|
||||||
emptyStateText = findViewById(R.id.boardsEmptyStateText)
|
emptyStateText = findViewById(R.id.boardsEmptyStateText)
|
||||||
initialProgress = findViewById(R.id.boardsInitialProgress)
|
initialProgress = findViewById(R.id.boardsInitialProgress)
|
||||||
createFab = findViewById(R.id.createBoardFab)
|
createFab = findViewById(R.id.createBoardFab)
|
||||||
|
drawerUsernameText = findViewById(R.id.drawerUsernameText)
|
||||||
|
drawerEmailText = findViewById(R.id.drawerEmailText)
|
||||||
|
drawerLoadingIndicator = findViewById(R.id.drawerLoadingIndicator)
|
||||||
|
drawerErrorText = findViewById(R.id.drawerErrorText)
|
||||||
|
drawerRetryButton = findViewById(R.id.drawerRetryButton)
|
||||||
|
drawerSettingsButton = findViewById(R.id.drawerSettingsButton)
|
||||||
|
drawerLogoutButton = findViewById(R.id.drawerLogoutButton)
|
||||||
|
drawerWorkspacesRecyclerView = findViewById(R.id.drawerWorkspacesRecyclerView)
|
||||||
|
|
||||||
|
applyDrawerWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecycler() {
|
private fun setupRecycler() {
|
||||||
@@ -96,9 +132,48 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
)
|
)
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
recyclerView.adapter = boardsAdapter
|
recyclerView.adapter = boardsAdapter
|
||||||
|
|
||||||
|
drawerAdapter = BoardsDrawerAdapter(
|
||||||
|
onWorkspaceClick = { workspace ->
|
||||||
|
if (viewModel.uiState.value.drawer.isWorkspaceInteractionEnabled) {
|
||||||
|
pendingWorkspaceSelectionId = workspace.id
|
||||||
|
viewModel.onWorkspaceSelected(workspace.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
drawerWorkspacesRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
drawerWorkspacesRecyclerView.adapter = drawerAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupInteractions() {
|
private fun setupInteractions() {
|
||||||
|
toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size)
|
||||||
|
toolbar.setNavigationContentDescription(R.string.drawer_workspaces)
|
||||||
|
toolbar.setNavigationOnClickListener {
|
||||||
|
drawerLayout.openDrawer(GravityCompat.START)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerRetryButton.setOnClickListener {
|
||||||
|
viewModel.retryDrawerData()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerSettingsButton.setOnClickListener {
|
||||||
|
drawerLayout.closeDrawer(GravityCompat.START)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerLogoutButton.setOnClickListener {
|
||||||
|
forceSignOutToLogin()
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerLayout.addDrawerListener(
|
||||||
|
object : DrawerLayout.SimpleDrawerListener() {
|
||||||
|
override fun onDrawerOpened(drawerView: View) {
|
||||||
|
if (drawerView.id == R.id.boardsDrawerContent) {
|
||||||
|
viewModel.loadDrawerDataIfStale()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
swipeRefresh.setOnRefreshListener {
|
swipeRefresh.setOnRefreshListener {
|
||||||
viewModel.refreshBoards()
|
viewModel.refreshBoards()
|
||||||
}
|
}
|
||||||
@@ -126,17 +201,84 @@ class BoardsActivity : AppCompatActivity() {
|
|||||||
.setPositiveButton(R.string.ok, null)
|
.setPositiveButton(R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BoardsUiEvent.ForceSignOut -> {
|
||||||
|
forceSignOutToLogin()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun render(state: BoardsUiState) {
|
private fun render(state: BoardsUiState) {
|
||||||
|
if (!hasRequestedInitialDrawerLoad) {
|
||||||
|
hasRequestedInitialDrawerLoad = true
|
||||||
|
viewModel.loadDrawerData()
|
||||||
|
}
|
||||||
|
|
||||||
boardsAdapter.submitBoards(state.boards)
|
boardsAdapter.submitBoards(state.boards)
|
||||||
swipeRefresh.isRefreshing = state.isRefreshing
|
swipeRefresh.isRefreshing = state.isRefreshing
|
||||||
initialProgress.visibility = if (state.isInitialLoading) View.VISIBLE else View.GONE
|
initialProgress.visibility = if (state.isInitialLoading) View.VISIBLE else View.GONE
|
||||||
emptyStateText.visibility = if (!state.isInitialLoading && state.boards.isEmpty()) View.VISIBLE else View.GONE
|
emptyStateText.visibility = if (!state.isInitialLoading && state.boards.isEmpty()) View.VISIBLE else View.GONE
|
||||||
createFab.isEnabled = !state.isMutating
|
createFab.isEnabled = !state.isMutating
|
||||||
|
|
||||||
|
val profile = state.drawer.profile
|
||||||
|
drawerUsernameText.text = profile?.displayName?.ifBlank { getString(R.string.drawer_profile_unavailable) }
|
||||||
|
?: getString(R.string.drawer_profile_unavailable)
|
||||||
|
drawerEmailText.text = profile?.email?.ifBlank { getString(R.string.drawer_profile_unavailable) }
|
||||||
|
?: getString(R.string.drawer_profile_unavailable)
|
||||||
|
|
||||||
|
drawerAdapter.submitItems(
|
||||||
|
workspaces = state.drawer.workspaces,
|
||||||
|
activeWorkspaceId = state.drawer.activeWorkspaceId,
|
||||||
|
)
|
||||||
|
|
||||||
|
drawerLoadingIndicator.visibility = if (state.drawer.isLoading) View.VISIBLE else View.GONE
|
||||||
|
val hasError = state.drawer.profileError != null || state.drawer.workspacesError != null
|
||||||
|
drawerErrorText.visibility = if (hasError) View.VISIBLE else View.GONE
|
||||||
|
drawerErrorText.text = state.drawer.workspacesError
|
||||||
|
?: state.drawer.profileError
|
||||||
|
?: getString(R.string.drawer_workspaces_unavailable)
|
||||||
|
drawerRetryButton.visibility = if (state.drawer.isRetryable) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
val isSwitchInFlight = state.drawer.isWorkspaceSwitchInFlight
|
||||||
|
if (lastDrawerSwitchInFlight && !isSwitchInFlight) {
|
||||||
|
val selectedWorkspace = pendingWorkspaceSelectionId
|
||||||
|
if (
|
||||||
|
selectedWorkspace != null &&
|
||||||
|
selectedWorkspace == state.drawer.activeWorkspaceId &&
|
||||||
|
state.drawer.errorCode == DrawerDataErrorCode.NONE
|
||||||
|
) {
|
||||||
|
drawerLayout.closeDrawer(Gravity.START)
|
||||||
|
}
|
||||||
|
pendingWorkspaceSelectionId = null
|
||||||
|
}
|
||||||
|
lastDrawerSwitchInFlight = isSwitchInFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyDrawerWidth() {
|
||||||
|
val maxWidthPx = resources.getDimensionPixelSize(R.dimen.boards_drawer_max_width)
|
||||||
|
val displayWidthPx = resources.displayMetrics.widthPixels
|
||||||
|
val computedWidth = minOf(displayWidthPx / 3, maxWidthPx)
|
||||||
|
drawerContent.layoutParams = drawerContent.layoutParams.apply {
|
||||||
|
width = computedWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun forceSignOutToLogin() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val baseUrl = sessionStore.getBaseUrl().orEmpty()
|
||||||
|
if (baseUrl.isNotBlank()) {
|
||||||
|
kotlinx.coroutines.withContext(Dispatchers.IO) {
|
||||||
|
apiKeyStore.invalidateApiKey(baseUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sessionStore.clearWorkspaceId()
|
||||||
|
val intent = Intent(this@BoardsActivity, MainActivity::class.java)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCreateBoardDialog() {
|
private fun showCreateBoardDialog() {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package space.hackenslacker.kanbn4droid.app.boards
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import space.hackenslacker.kanbn4droid.app.R
|
||||||
|
|
||||||
|
class BoardsDrawerAdapter(
|
||||||
|
private val onWorkspaceClick: (WorkspaceSummary) -> Unit,
|
||||||
|
) : RecyclerView.Adapter<BoardsDrawerAdapter.WorkspaceViewHolder>() {
|
||||||
|
private val workspaces = mutableListOf<WorkspaceSummary>()
|
||||||
|
private var activeWorkspaceId: String? = null
|
||||||
|
|
||||||
|
fun submitItems(workspaces: List<WorkspaceSummary>, activeWorkspaceId: String?) {
|
||||||
|
this.workspaces.clear()
|
||||||
|
this.workspaces.addAll(workspaces)
|
||||||
|
this.activeWorkspaceId = activeWorkspaceId
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WorkspaceViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_workspace_drawer, parent, false)
|
||||||
|
return WorkspaceViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: WorkspaceViewHolder, position: Int) {
|
||||||
|
val workspace = workspaces[position]
|
||||||
|
holder.bind(
|
||||||
|
workspace = workspace,
|
||||||
|
isSelected = workspace.id == activeWorkspaceId,
|
||||||
|
onWorkspaceClick = onWorkspaceClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = workspaces.size
|
||||||
|
|
||||||
|
class WorkspaceViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
private val workspaceTitleText: TextView = itemView.findViewById(R.id.workspaceTitleText)
|
||||||
|
|
||||||
|
fun bind(
|
||||||
|
workspace: WorkspaceSummary,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onWorkspaceClick: (WorkspaceSummary) -> Unit,
|
||||||
|
) {
|
||||||
|
workspaceTitleText.text = workspace.name
|
||||||
|
itemView.isSelected = isSelected
|
||||||
|
itemView.isActivated = isSelected
|
||||||
|
workspaceTitleText.isSelected = isSelected
|
||||||
|
workspaceTitleText.isActivated = isSelected
|
||||||
|
itemView.setOnClickListener {
|
||||||
|
onWorkspaceClick(workspace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ interface BoardsUiEvent {
|
|||||||
|
|
||||||
class BoardsViewModel(
|
class BoardsViewModel(
|
||||||
private val repository: BoardsRepository,
|
private val repository: BoardsRepository,
|
||||||
|
private val nowProvider: () -> Long = { System.currentTimeMillis() },
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _uiState = MutableStateFlow(BoardsUiState())
|
private val _uiState = MutableStateFlow(BoardsUiState())
|
||||||
val uiState: StateFlow<BoardsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<BoardsUiState> = _uiState.asStateFlow()
|
||||||
@@ -37,6 +38,8 @@ class BoardsViewModel(
|
|||||||
private val _events = MutableSharedFlow<BoardsUiEvent>()
|
private val _events = MutableSharedFlow<BoardsUiEvent>()
|
||||||
val events: SharedFlow<BoardsUiEvent> = _events.asSharedFlow()
|
val events: SharedFlow<BoardsUiEvent> = _events.asSharedFlow()
|
||||||
|
|
||||||
|
private var lastDrawerLoadAtMillis: Long? = null
|
||||||
|
|
||||||
fun loadBoards() {
|
fun loadBoards() {
|
||||||
fetchBoards(initial = true)
|
fetchBoards(initial = true)
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,15 @@ class BoardsViewModel(
|
|||||||
fetchDrawerData()
|
fetchDrawerData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadDrawerDataIfStale() {
|
||||||
|
val now = nowProvider()
|
||||||
|
val isStale = lastDrawerLoadAtMillis?.let { now - it >= DRAWER_STALE_MS } ?: true
|
||||||
|
if (!isStale) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchDrawerData()
|
||||||
|
}
|
||||||
|
|
||||||
fun retryDrawerData() {
|
fun retryDrawerData() {
|
||||||
fetchDrawerData()
|
fetchDrawerData()
|
||||||
}
|
}
|
||||||
@@ -246,6 +258,8 @@ class BoardsViewModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastDrawerLoadAtMillis = nowProvider()
|
||||||
|
|
||||||
if (result.errorCode == DrawerDataErrorCode.UNAUTHORIZED) {
|
if (result.errorCode == DrawerDataErrorCode.UNAUTHORIZED) {
|
||||||
_events.emit(BoardsUiEvent.ForceSignOut)
|
_events.emit(BoardsUiEvent.ForceSignOut)
|
||||||
return@launch
|
return@launch
|
||||||
@@ -288,4 +302,8 @@ class BoardsViewModel(
|
|||||||
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val DRAWER_STALE_MS = 2 * 60 * 1000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/boardsDrawerContent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/colorSurface"
|
android:background="?attr/colorSurface"
|
||||||
@@ -50,6 +51,33 @@
|
|||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="8dp" />
|
android:paddingBottom="8dp" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/drawerLoadingIndicator"
|
||||||
|
style="@style/Widget.AppCompat.ProgressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/drawerErrorText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/drawer_workspaces_unavailable"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/drawerRetryButton"
|
||||||
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/drawer_retry"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/drawerLogoutButton"
|
android:id="@+id/drawerLogoutButton"
|
||||||
style="?attr/materialButtonOutlinedStyle"
|
style="?attr/materialButtonOutlinedStyle"
|
||||||
|
|||||||
@@ -272,6 +272,36 @@ class BoardsViewModelTest {
|
|||||||
assertEquals("Alice", drawer.profile?.displayName)
|
assertEquals("Alice", drawer.profile?.displayName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadDrawerDataIfStaleSkipsFreshDataAndReloadsWhenStale() = runTest {
|
||||||
|
var now = 1_000L
|
||||||
|
val api = FakeBoardsApiClient().apply {
|
||||||
|
usersMeResult = BoardsApiResult.Success(DrawerProfile(displayName = "Alice", email = null))
|
||||||
|
workspacesResult = BoardsApiResult.Success(listOf(WorkspaceSummary("ws-1", "Main")))
|
||||||
|
}
|
||||||
|
val viewModel = newViewModel(apiClient = api, nowProvider = { now })
|
||||||
|
|
||||||
|
viewModel.loadDrawerData()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(1, api.listWorkspacesCalls)
|
||||||
|
assertEquals(1, api.getCurrentUserCalls)
|
||||||
|
|
||||||
|
now += 10_000L
|
||||||
|
viewModel.loadDrawerDataIfStale()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(1, api.listWorkspacesCalls)
|
||||||
|
assertEquals(1, api.getCurrentUserCalls)
|
||||||
|
|
||||||
|
now += 180_000L
|
||||||
|
viewModel.loadDrawerDataIfStale()
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(2, api.listWorkspacesCalls)
|
||||||
|
assertEquals(2, api.getCurrentUserCalls)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun loadDrawerDataFallbackWorkspacePersistsFirstAndTriggersBoardsRefresh() = runTest {
|
fun loadDrawerDataFallbackWorkspacePersistsFirstAndTriggersBoardsRefresh() = runTest {
|
||||||
val api = FakeBoardsApiClient().apply {
|
val api = FakeBoardsApiClient().apply {
|
||||||
@@ -372,6 +402,7 @@ class BoardsViewModelTest {
|
|||||||
private fun newViewModel(
|
private fun newViewModel(
|
||||||
apiClient: FakeBoardsApiClient,
|
apiClient: FakeBoardsApiClient,
|
||||||
sessionStore: InMemorySessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1"),
|
sessionStore: InMemorySessionStore = InMemorySessionStore("https://kan.bn/", workspaceId = "ws-1"),
|
||||||
|
nowProvider: () -> Long = { System.currentTimeMillis() },
|
||||||
): BoardsViewModel {
|
): BoardsViewModel {
|
||||||
val repository = BoardsRepository(
|
val repository = BoardsRepository(
|
||||||
sessionStore = sessionStore,
|
sessionStore = sessionStore,
|
||||||
@@ -379,7 +410,7 @@ class BoardsViewModelTest {
|
|||||||
apiClient = apiClient,
|
apiClient = apiClient,
|
||||||
ioDispatcher = UnconfinedTestDispatcher(),
|
ioDispatcher = UnconfinedTestDispatcher(),
|
||||||
)
|
)
|
||||||
return BoardsViewModel(repository)
|
return BoardsViewModel(repository, nowProvider = nowProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InMemorySessionStore(
|
private class InMemorySessionStore(
|
||||||
@@ -434,9 +465,12 @@ class BoardsViewModelTest {
|
|||||||
|
|
||||||
var lastDeletedId: String? = null
|
var lastDeletedId: String? = null
|
||||||
var listBoardsCalls: Int = 0
|
var listBoardsCalls: Int = 0
|
||||||
|
var listWorkspacesCalls: Int = 0
|
||||||
|
var getCurrentUserCalls: Int = 0
|
||||||
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
override suspend fun healthCheck(baseUrl: String, apiKey: String): AuthResult = AuthResult.Success
|
||||||
|
|
||||||
override suspend fun getCurrentUser(baseUrl: String, apiKey: String): BoardsApiResult<DrawerProfile> {
|
override suspend fun getCurrentUser(baseUrl: String, apiKey: String): BoardsApiResult<DrawerProfile> {
|
||||||
|
getCurrentUserCalls += 1
|
||||||
return usersMeResults.removeFirstOrNull() ?: usersMeResult
|
return usersMeResults.removeFirstOrNull() ?: usersMeResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +478,7 @@ class BoardsViewModelTest {
|
|||||||
baseUrl: String,
|
baseUrl: String,
|
||||||
apiKey: String,
|
apiKey: String,
|
||||||
): BoardsApiResult<List<WorkspaceSummary>> {
|
): BoardsApiResult<List<WorkspaceSummary>> {
|
||||||
|
listWorkspacesCalls += 1
|
||||||
return workspacesResults.removeFirstOrNull() ?: workspacesResult
|
return workspacesResults.removeFirstOrNull() ?: workspacesResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user