test: cover board detail create and filter user flows
This commit is contained in:
@@ -9,10 +9,12 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.Espresso.pressBack
|
||||
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.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
|
||||
@@ -28,6 +30,7 @@ import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.chip.Chip
|
||||
import java.text.DateFormat
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.util.ArrayDeque
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
@@ -65,6 +68,7 @@ class BoardDetailFlowTest {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
observedStates.clear()
|
||||
MainActivity.dependencies.clear()
|
||||
MainActivity.dependencies.sessionStoreFactory = { InMemorySessionStore("https://kan.bn/") }
|
||||
MainActivity.dependencies.apiKeyStoreFactory = { InMemoryApiKeyStore("api") }
|
||||
@@ -357,6 +361,203 @@ class BoardDetailFlowTest {
|
||||
onView(withText("Add new card")).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fabChooser_addList_createAndValidationFlow() {
|
||||
launchBoardDetail()
|
||||
|
||||
onView(withId(R.id.boardDetailCreateFab)).perform(click())
|
||||
onView(withText(R.string.add_new_list)).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.add_new_list)).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
|
||||
onView(withText(R.string.add_list)).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.list_title_required)).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
|
||||
onView(withId(R.id.addListTitleInput)).inRoot(isDialog()).perform(replaceText("Created List"))
|
||||
onView(withText(R.string.add_list)).inRoot(isDialog()).perform(click())
|
||||
|
||||
awaitCondition {
|
||||
defaultDataSource.createListCalls == 1 && observedStates.lastOrNull()?.isMutating == false
|
||||
}
|
||||
|
||||
assertEquals(1, defaultDataSource.createListCalls)
|
||||
assertEquals("board-1", defaultDataSource.lastCreateListBoardId)
|
||||
assertEquals("Created List", defaultDataSource.lastCreateListTitle)
|
||||
assertTrue(
|
||||
observedStates.lastOrNull()
|
||||
?.boardDetail
|
||||
?.lists
|
||||
?.any { it.title == "Created List" } == true,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fabChooser_addCard_createWithTagsAndClearDate() {
|
||||
defaultDataSource.currentDetail = detailCreateCardDialogTagCatalog()
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
onView(withId(R.id.boardDetailCreateFab)).perform(click())
|
||||
onView(withText(R.string.add_new_card)).inRoot(isDialog()).perform(click())
|
||||
|
||||
onView(withId(R.id.addCardTitleInput)).inRoot(isDialog()).perform(replaceText("Created Card"))
|
||||
onView(withId(R.id.addCardDescriptionInput)).inRoot(isDialog()).perform(replaceText("Body"))
|
||||
onView(withText("Backend")).inRoot(isDialog()).perform(click())
|
||||
onView(withId(R.id.addCardDueDateText)).inRoot(isDialog()).perform(click())
|
||||
onView(withText(android.R.string.ok)).inRoot(isDialog()).perform(click())
|
||||
awaitCondition {
|
||||
observedStates.lastOrNull()?.addCardDueDate != null
|
||||
}
|
||||
scenario.onActivity { }
|
||||
onView(withId(R.id.addCardClearDueDateAction)).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
onView(withId(R.id.addCardClearDueDateAction)).inRoot(isDialog()).perform(click())
|
||||
awaitCondition {
|
||||
observedStates.lastOrNull()?.addCardDueDate == null
|
||||
}
|
||||
onView(withText(R.string.add_card)).inRoot(isDialog()).perform(click())
|
||||
|
||||
awaitCondition {
|
||||
defaultDataSource.createCardCalls == 1 && observedStates.lastOrNull()?.isMutating == false
|
||||
}
|
||||
|
||||
assertEquals(1, defaultDataSource.createCardCalls)
|
||||
assertEquals("list-1", defaultDataSource.lastCreateCardListId)
|
||||
assertEquals("Created Card", defaultDataSource.lastCreateCardTitle)
|
||||
assertEquals("Body", defaultDataSource.lastCreateCardDescription)
|
||||
assertEquals(setOf("tag-1"), defaultDataSource.lastCreateCardTagIds)
|
||||
assertNull(defaultDataSource.lastCreateCardDueDate)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fabChooser_zeroLists_disablesAddCard_andShowsHelperMessage() {
|
||||
defaultDataSource.currentDetail = BoardDetail(id = "board-1", title = "Board", lists = emptyList())
|
||||
launchBoardDetail()
|
||||
|
||||
onView(withId(R.id.boardDetailCreateFab)).perform(click())
|
||||
onView(withText(R.string.add_new_card)).inRoot(isDialog()).check(matches(not(isEnabled())))
|
||||
onView(withText(R.string.create_a_list_first_to_add_cards)).inRoot(isDialog()).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filters_applyTagAnyAndSearchAnd_keepAllListsVisible() {
|
||||
defaultDataSource.currentDetail = detailFilterAndSearchThreeLists()
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
onView(withContentDescription("Filter by tag")).perform(click())
|
||||
onView(withText("Backend")).inRoot(isDialog()).perform(click())
|
||||
onView(withText("Mobile")).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.filter)).inRoot(isDialog()).perform(click())
|
||||
|
||||
onView(withContentDescription("Search")).perform(click())
|
||||
onView(withId(R.id.searchTitleInput)).inRoot(isDialog()).perform(replaceText("Duke"))
|
||||
onView(withText(R.string.search)).inRoot(isDialog()).perform(click())
|
||||
|
||||
onView(withText("Duke Backend")).check(matches(isDisplayed()))
|
||||
scenario.onActivity { activity ->
|
||||
activity.findViewById<ViewPager2>(R.id.boardDetailPager).setCurrentItem(1, false)
|
||||
}
|
||||
onView(withText("Duke Mobile")).check(matches(isDisplayed()))
|
||||
scenario.onActivity { activity ->
|
||||
activity.findViewById<ViewPager2>(R.id.boardDetailPager).setCurrentItem(2, false)
|
||||
}
|
||||
onView(withText("Done")).check(matches(isDisplayed()))
|
||||
onView(withText(R.string.board_detail_empty_list)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun filterAndSearchActions_showActiveTintWhenCriteriaApplied_andResetOnClear() {
|
||||
defaultDataSource.currentDetail = detailFilterAndSearchThreeLists()
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
onView(withContentDescription("Filter by tag")).perform(click())
|
||||
onView(withText("Backend")).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.filter)).inRoot(isDialog()).perform(click())
|
||||
|
||||
onView(withContentDescription("Search")).perform(click())
|
||||
onView(withId(R.id.searchTitleInput)).inRoot(isDialog()).perform(replaceText("Duke"))
|
||||
onView(withText(R.string.search)).inRoot(isDialog()).perform(click())
|
||||
|
||||
scenario.onActivity { activity ->
|
||||
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
|
||||
val expectedActive = MaterialColors.getColor(
|
||||
activity,
|
||||
com.google.android.material.R.attr.colorPrimary,
|
||||
Color.BLUE,
|
||||
)
|
||||
assertEquals(expectedActive, menu.findItem(R.id.actionFilterByTag).iconTintList?.defaultColor)
|
||||
assertEquals(expectedActive, menu.findItem(R.id.actionSearch).iconTintList?.defaultColor)
|
||||
}
|
||||
|
||||
onView(withContentDescription("Filter by tag")).perform(click())
|
||||
onView(withContentDescription("Search")).perform(click())
|
||||
|
||||
scenario.onActivity { activity ->
|
||||
val menu = activity.findViewById<com.google.android.material.appbar.MaterialToolbar>(R.id.boardDetailToolbar).menu
|
||||
val expectedInactive = MaterialColors.getColor(
|
||||
activity,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant,
|
||||
Color.BLACK,
|
||||
)
|
||||
assertEquals(expectedInactive, menu.findItem(R.id.actionFilterByTag).iconTintList?.defaultColor)
|
||||
assertEquals(expectedInactive, menu.findItem(R.id.actionSearch).iconTintList?.defaultColor)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun activeFilterOrSearchIconTap_clearsCriterionImmediately() {
|
||||
defaultDataSource.currentDetail = detailFilterAndSearchThreeLists()
|
||||
val scenario = launchBoardDetail()
|
||||
|
||||
onView(withContentDescription("Filter by tag")).perform(click())
|
||||
onView(withText("Backend")).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.filter)).inRoot(isDialog()).perform(click())
|
||||
onView(withText("Duke Mobile")).check(doesNotExist())
|
||||
|
||||
onView(withContentDescription("Filter by tag")).perform(click())
|
||||
scenario.onActivity { activity ->
|
||||
activity.findViewById<ViewPager2>(R.id.boardDetailPager).setCurrentItem(1, false)
|
||||
}
|
||||
onView(withText("Duke Mobile")).check(matches(isDisplayed()))
|
||||
|
||||
onView(withContentDescription("Search")).perform(click())
|
||||
onView(withId(R.id.searchTitleInput)).inRoot(isDialog()).perform(replaceText("Backend"))
|
||||
onView(withText(R.string.search)).inRoot(isDialog()).perform(click())
|
||||
onView(withText(R.string.board_detail_empty_list)).check(matches(isDisplayed()))
|
||||
|
||||
onView(withContentDescription("Search")).perform(click())
|
||||
scenario.onActivity { activity ->
|
||||
activity.findViewById<ViewPager2>(R.id.boardDetailPager).setCurrentItem(1, false)
|
||||
}
|
||||
onView(withText("Duke Mobile")).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun selectionMode_hidesFilterAndSearchActions() {
|
||||
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
|
||||
assertNull(menu.findItem(R.id.actionFilterByTag))
|
||||
assertNull(menu.findItem(R.id.actionSearch))
|
||||
assertNotNull(menu.findItem(R.id.actionSelectAll))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun backArrowAndSystemBack_clearSelectionBeforeNavigation() {
|
||||
launchBoardDetail()
|
||||
onView(withText("Card 1")).perform(longClick())
|
||||
onView(withId(R.id.actionSelectAll)).check(matches(isDisplayed()))
|
||||
onView(withContentDescription(androidx.appcompat.R.string.abc_action_bar_up_description)).perform(click())
|
||||
onView(withId(R.id.actionFilterByTag)).check(matches(isDisplayed()))
|
||||
|
||||
launchBoardDetail()
|
||||
onView(withText("Card 1")).perform(longClick())
|
||||
onView(withId(R.id.actionSelectAll)).check(matches(isDisplayed()))
|
||||
pressBack()
|
||||
onView(withId(R.id.actionFilterByTag)).check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingBoardIdShowsBlockingDialogAndFinishes() {
|
||||
val scenario = launchBoardDetail(boardId = null)
|
||||
@@ -682,12 +883,37 @@ class BoardDetailFlowTest {
|
||||
var deleteGate: CompletableDeferred<Unit>? = null
|
||||
var moveCalls: Int = 0
|
||||
var deleteCalls: Int = 0
|
||||
var createListCalls: Int = 0
|
||||
var createCardCalls: Int = 0
|
||||
var lastMoveCardIds: Set<String> = emptySet()
|
||||
var lastDeleteCardIds: Set<String> = emptySet()
|
||||
var lastMoveTargetListId: String? = null
|
||||
var lastCreateListBoardId: String? = null
|
||||
var lastCreateListTitle: String? = null
|
||||
var lastCreateCardListId: String? = null
|
||||
var lastCreateCardTitle: String? = null
|
||||
var lastCreateCardDescription: String? = null
|
||||
var lastCreateCardDueDate: LocalDate? = null
|
||||
var lastCreateCardTagIds: Set<String> = emptySet()
|
||||
var createListResult: BoardsApiResult<CreatedEntityRef> = BoardsApiResult.Success(CreatedEntityRef("created-list"))
|
||||
var createCardResult: BoardsApiResult<CreatedEntityRef> = BoardsApiResult.Success(CreatedEntityRef("created-card"))
|
||||
|
||||
override suspend fun createList(boardPublicId: String, title: String): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("Not implemented in test fake")
|
||||
createListCalls += 1
|
||||
lastCreateListBoardId = boardPublicId
|
||||
lastCreateListTitle = title
|
||||
val result = createListResult
|
||||
if (result is BoardsApiResult.Success) {
|
||||
val newId = result.value.publicId?.trim().orEmpty().ifBlank { "created-list-${createListCalls}" }
|
||||
currentDetail = currentDetail.copy(
|
||||
lists = currentDetail.lists + BoardListDetail(
|
||||
id = newId,
|
||||
title = title,
|
||||
cards = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun createCard(
|
||||
@@ -697,7 +923,47 @@ class BoardDetailFlowTest {
|
||||
dueDate: LocalDate?,
|
||||
tagPublicIds: Collection<String>,
|
||||
): BoardsApiResult<CreatedEntityRef> {
|
||||
return BoardsApiResult.Failure("Not implemented in test fake")
|
||||
createCardCalls += 1
|
||||
lastCreateCardListId = listPublicId
|
||||
lastCreateCardTitle = title
|
||||
lastCreateCardDescription = description
|
||||
lastCreateCardDueDate = dueDate
|
||||
lastCreateCardTagIds = tagPublicIds.toSet()
|
||||
|
||||
val result = createCardResult
|
||||
if (result is BoardsApiResult.Success) {
|
||||
val newId = result.value.publicId?.trim().orEmpty().ifBlank { "created-card-${createCardCalls}" }
|
||||
val knownTagsById = currentDetail.lists
|
||||
.asSequence()
|
||||
.flatMap { list -> list.cards.asSequence() }
|
||||
.flatMap { card -> card.tags.asSequence() }
|
||||
.associateBy { tag -> tag.id }
|
||||
val newTags = tagPublicIds.mapNotNull { knownTagsById[it] }
|
||||
val dueAt = dueDate
|
||||
?.atStartOfDay(ZoneId.systemDefault())
|
||||
?.toInstant()
|
||||
?.toEpochMilli()
|
||||
|
||||
currentDetail = currentDetail.copy(
|
||||
lists = currentDetail.lists.map { list ->
|
||||
if (list.id == listPublicId) {
|
||||
list.copy(
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = newId,
|
||||
title = title,
|
||||
tags = newTags,
|
||||
dueAtEpochMillis = dueAt,
|
||||
),
|
||||
) + list.cards,
|
||||
)
|
||||
} else {
|
||||
list
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun getBoardDetail(boardId: String): BoardsApiResult<BoardDetail> {
|
||||
@@ -1026,5 +1292,74 @@ class BoardDetailFlowTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun detailCreateCardDialogTagCatalog(): BoardDetail {
|
||||
return BoardDetail(
|
||||
id = "board-1",
|
||||
title = "Board",
|
||||
lists = listOf(
|
||||
BoardListDetail(
|
||||
id = "list-1",
|
||||
title = "To Do",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "card-1",
|
||||
title = "Existing",
|
||||
tags = listOf(
|
||||
BoardTagSummary("tag-1", "Backend", "#008080"),
|
||||
BoardTagSummary("tag-2", "Mobile", "#990000"),
|
||||
),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun detailFilterAndSearchThreeLists(): BoardDetail {
|
||||
return BoardDetail(
|
||||
id = "board-1",
|
||||
title = "Board",
|
||||
lists = listOf(
|
||||
BoardListDetail(
|
||||
id = "list-1",
|
||||
title = "To Do",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "todo-duke-backend",
|
||||
title = "Duke Backend",
|
||||
tags = listOf(BoardTagSummary("tag-1", "Backend", "#008080")),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
BoardListDetail(
|
||||
id = "list-2",
|
||||
title = "Doing",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "doing-duke-mobile",
|
||||
title = "Duke Mobile",
|
||||
tags = listOf(BoardTagSummary("tag-2", "Mobile", "#009900")),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
BoardListDetail(
|
||||
id = "list-3",
|
||||
title = "Done",
|
||||
cards = listOf(
|
||||
BoardCardSummary(
|
||||
id = "done-archived",
|
||||
title = "Archived",
|
||||
tags = listOf(BoardTagSummary("tag-3", "Ops", "#333333")),
|
||||
dueAtEpochMillis = null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user