0
0
mirror of https://github.com/ankidroid/Anki-Android.git synced 2024-09-20 12:02:16 +02:00

Fix deck picker options flickering + error on full sync

https://github.com/ankidroid/Anki-Android/pull/11849#issuecomment-1211775736

Android's onCreateOptionsMenu does not play well with coroutines, as
it expects the menu to have been fully configured by the time the routine
returns. This results in flicker, as the menu gets blanked out, and then
configured a moment later when the coroutine runs. To work around this,
the current state is stored in the deck picker, so that we can redraw the
menu immediately, and then make any potential changes in the background.

Other changes:
- refactored onCreateOptionsMenu to make it simpler
- instead of the sdCardAvailable checks (which I assume is a proxy for
"col is available"), the entire menu is wrapped in a group, and the
visibility of the group is toggled depending on whether the col is available
or not. This also fixes the error on a full sync.
- there are three sets of unit tests (one for search icon, one for sync icon,
one for entire menu) that have been a pain since I originally introduced
this PR, and and I've sunk a number of hours into trying to get them to work
properly at this point. The issue appears to be that when mixing coroutine
calls and invalidateOptionsMenu(), onCreateOptionsMenu() is not getting called
before trying to await the job, leading to a hang or stale data. I tried
advancing robolectric, but it did not help. Maybe someone more experienced
in this area can figure it out, but for now I've changed these routines
to be more of a unit test and less of an integration test: rather than
checking the menu itself, they directly invoke the function that updates
the menu state, and check the state instead. This takes onCreateOptionsMenu()
out of the loop, and avoids the problems (and probably allows these tests
to be re-enabled on Windows as well). The sync tests I've removed, as the
entire menu is hidden/shown now when the col is closed, so they are redundant.
This commit is contained in:
Damien Elmes 2022-08-12 18:09:04 +10:00 committed by Mike Hardy
parent c69f273496
commit 06741b6c2d
5 changed files with 213 additions and 268 deletions

View File

@ -96,6 +96,7 @@ import com.ichi2.async.Connection.CancellableTaskListener
import com.ichi2.async.Connection.ConflictResolution
import com.ichi2.compat.CompatHelper.Companion.sdkVersion
import com.ichi2.libanki.*
import com.ichi2.libanki.Collection
import com.ichi2.libanki.Collection.CheckDatabaseResult
import com.ichi2.libanki.importer.AnkiPackageImporter
import com.ichi2.libanki.sched.AbstractDeckTreeNode
@ -182,6 +183,10 @@ open class DeckPicker :
private var mStartupError = false
private var mEmptyCardTask: Cancellable? = null
/** See [OptionsMenuState]. */
@VisibleForTesting
var optionsMenuState: OptionsMenuState? = null
@JvmField
@VisibleForTesting
var mDueTree: List<TreeNode<AbstractDeckTreeNode>>? = null
@ -579,51 +584,47 @@ open class DeckPicker :
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
Timber.d("onCreateOptionsMenu()")
mFloatingActionMenu.closeFloatingActionMenu()
menuInflater.inflate(R.menu.deck_picker, menu)
setupSearchIcon(menu.findItem(R.id.deck_picker_action_filter))
// redraw menu synchronously to avoid flicker
updateMenuFromState(menu)
// ...then launch a task to possibly update the visible icons.
// Store the job so that tests can easily await it. In the future
// this may be better done by injecting a custom test scheduler
// into CollectionManager, and awaiting that.
createMenuJob = launchCatchingTask {
val haveCol = withOpenColOrNull { true } ?: false
if (!haveCol) {
// avoid showing the menu if the collection is not open
return@launchCatchingTask
updateMenuState()
updateMenuFromState(menu)
}
return super.onCreateOptionsMenu(menu)
}
private fun setupSearchIcon(menuItem: MenuItem) {
menuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
// When SearchItem is expanded
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
Timber.i("DeckPicker:: SearchItem opened")
// Hide the floating action button if it is visible
mFloatingActionMenu.hideFloatingActionButton()
return true
}
Timber.d("onCreateOptionsMenu()")
mFloatingActionMenu.closeFloatingActionMenu()
menuInflater.inflate(R.menu.deck_picker, menu)
val sdCardAvailable = AnkiDroidApp.isSdCardMounted()
menu.findItem(R.id.action_sync).isEnabled = sdCardAvailable
menu.findItem(R.id.action_new_filtered_deck).isEnabled = sdCardAvailable
menu.findItem(R.id.action_check_database).isEnabled = sdCardAvailable
menu.findItem(R.id.action_check_media).isEnabled = sdCardAvailable
menu.findItem(R.id.action_empty_cards).isEnabled = sdCardAvailable
// When SearchItem is collapsed
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
Timber.i("DeckPicker:: SearchItem closed")
// Show the floating action button if it is hidden
mFloatingActionMenu.showFloatingActionButton()
return true
}
})
searchDecksIcon = menu.findItem(R.id.deck_picker_action_filter)
updateSearchDecksIconVisibility()
searchDecksIcon!!.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
// When SearchItem is expanded
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
Timber.i("DeckPicker:: SearchItem opened")
// Hide the floating action button if it is visible
mFloatingActionMenu.hideFloatingActionButton()
return true
}
// When SearchItem is collapsed
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
Timber.i("DeckPicker:: SearchItem closed")
// Show the floating action button if it is hidden
mFloatingActionMenu.showFloatingActionButton()
return true
}
})
mToolbarSearchView = searchDecksIcon!!.actionView as SearchView
mToolbarSearchView!!.queryHint = getString(R.string.search_decks)
mToolbarSearchView!!.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
(menuItem.actionView as SearchView).run {
queryHint = getString(R.string.search_decks)
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
mToolbarSearchView!!.clearFocus()
clearFocus()
return true
}
@ -633,77 +634,87 @@ open class DeckPicker :
return true
}
})
}
searchDecksIcon = menuItem
}
displaySyncBadge(menu)
private fun updateMenuFromState(menu: Menu) {
menu.setGroupVisible(R.id.allItems, optionsMenuState != null)
optionsMenuState?.run {
menu.findItem(R.id.deck_picker_action_filter).isVisible = searchIcon
updateUndoIconFromState(menu.findItem(R.id.action_undo), undoIcon)
updateSyncIconFromState(menu.findItem(R.id.action_sync), syncIcon)
}
}
// Show / hide undo
val undoName = withOpenColOrNull {
if (fragmented || !undoAvailable()) {
null
} else {
undoName(resources)
}
}
if (undoName == null) {
menu.findItem(R.id.action_undo).isVisible = false
private fun updateUndoIconFromState(menuItem: MenuItem, undoTitle: String?) {
menuItem.run {
if (undoTitle != null) {
isVisible = true
title = resources.getString(R.string.studyoptions_congrats_undo, undoTitle)
} else {
val res = resources
menu.findItem(R.id.action_undo).isVisible = true
val undo = res.getString(R.string.studyoptions_congrats_undo, undoName)
menu.findItem(R.id.action_undo).title = undo
isVisible = false
}
updateSearchDecksIconVisibility()
}
return super.onCreateOptionsMenu(menu)
}
@VisibleForTesting
suspend fun updateSearchDecksIconVisibility() {
val visible = withOpenColOrNull { decks.count() >= 10 } ?: false
searchDecksIcon?.isVisible = visible
}
@VisibleForTesting
protected open suspend fun displaySyncBadge(menu: Menu) {
val auth = syncAuth()
val syncStatus = withOpenColOrNull {
SyncStatus.getSyncStatus(this, auth)
}
if (syncStatus == null) {
return
}
val syncMenu = menu.findItem(R.id.action_sync)
when (syncStatus) {
SyncStatus.BADGE_DISABLED, SyncStatus.NO_CHANGES, SyncStatus.INCONCLUSIVE -> {
syncMenu?.let {
BadgeDrawableBuilder.removeBadge(it)
it.setTitle(R.string.button_sync)
}
private fun updateSyncIconFromState(menuItem: MenuItem, syncIcon: SyncIconState) {
when (syncIcon) {
SyncIconState.Normal -> {
BadgeDrawableBuilder.removeBadge(menuItem)
menuItem.setTitle(R.string.button_sync)
}
SyncStatus.HAS_CHANGES -> {
// Light orange icon
SyncIconState.PendingChanges -> {
BadgeDrawableBuilder(resources)
.withColor(ContextCompat.getColor(this, R.color.badge_warning))
.replaceBadge(syncMenu)
syncMenu.setTitle(R.string.button_sync)
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_warning))
.replaceBadge(menuItem)
menuItem.setTitle(R.string.button_sync)
}
SyncStatus.NO_ACCOUNT, SyncStatus.FULL_SYNC -> {
if (syncStatus === SyncStatus.NO_ACCOUNT) {
syncMenu.setTitle(R.string.sync_menu_title_no_account)
} else if (syncStatus === SyncStatus.FULL_SYNC) {
syncMenu.setTitle(R.string.sync_menu_title_full_sync)
}
// Orange-red icon with exclamation mark
SyncIconState.FullSync, SyncIconState.NotLoggedIn -> {
BadgeDrawableBuilder(resources)
.withText('!')
.withColor(ContextCompat.getColor(this, R.color.badge_error))
.replaceBadge(syncMenu)
.withColor(ContextCompat.getColor(this@DeckPicker, R.color.badge_error))
.replaceBadge(menuItem)
if (syncIcon == SyncIconState.FullSync) {
menuItem.setTitle(R.string.sync_menu_title_full_sync)
} else {
menuItem.setTitle(R.string.sync_menu_title_no_account)
}
}
}
}
@VisibleForTesting
suspend fun updateMenuState() {
optionsMenuState = withOpenColOrNull {
val searchIcon = decks.count() >= 10
val undoIcon = undoName(resources).let {
if (it.isEmpty()) {
null
} else {
it
}
}
val syncIcon = fetchSyncStatus(col)
OptionsMenuState(searchIcon, undoIcon, syncIcon)
}
}
private fun fetchSyncStatus(col: Collection): SyncIconState {
val auth = syncAuth()
val syncStatus = SyncStatus.getSyncStatus(col, auth)
return when (syncStatus) {
SyncStatus.BADGE_DISABLED, SyncStatus.NO_CHANGES, SyncStatus.INCONCLUSIVE -> {
SyncIconState.Normal
}
SyncStatus.HAS_CHANGES -> {
SyncIconState.PendingChanges
}
SyncStatus.NO_ACCOUNT -> SyncIconState.NotLoggedIn
SyncStatus.FULL_SYNC -> SyncIconState.FullSync
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
mFloatingActionMenu.closeFloatingActionMenu()
@ -2152,6 +2163,7 @@ open class DeckPicker :
@RustCleanup("backup with 5 minute timer, instead of deck list refresh")
private fun updateDeckList(quick: Boolean) {
invalidateOptionsMenu()
if (!BackendFactory.defaultLegacySchema) {
// uses user's desktop settings to determine whether a backup
// actually happens
@ -2241,10 +2253,6 @@ open class DeckPicker :
scrollDecklistToDeck(current)
mFocusedDeck = current
}
launchCatchingTask {
updateSearchDecksIconVisibility()
}
}
// Callback to show study options for currently selected deck
@ -2727,3 +2735,23 @@ open class DeckPicker :
}
}
}
/** Android's onCreateOptionsMenu does not play well with coroutines, as
* it expects the menu to have been fully configured by the time the routine
* returns. This results in flicker, as the menu gets blanked out, and then
* configured a moment later when the coroutine runs. To work around this,
* the current state is stored in the deck picker so that we can redraw the
* menu immediately. */
data class OptionsMenuState(
var searchIcon: Boolean,
/** If undo is available, a string describing the action. */
var undoIcon: String?,
var syncIcon: SyncIconState
)
enum class SyncIconState {
Normal,
PendingChanges,
FullSync,
NotLoggedIn
}

View File

@ -1,60 +1,58 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:ankidroid="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/deck_picker_action_filter"
android:icon="@drawable/ic_search_white"
android:title="@string/search_decks"
ankidroid:actionViewClass="androidx.appcompat.widget.SearchView"
ankidroid:showAsAction="always|collapseActionView"/>
<!--
We want sync on the right, to be consistent with past version: #7737.
We're OK to do this as we're using "always" so it won't be hidden
-->
<item
android:id="@+id/action_sync"
android:icon="@drawable/ic_sync"
android:title="@string/button_sync"
ankidroid:showAsAction="always"/>
<item
android:id="@+id/action_undo"
android:title="@string/undo_action_review"
ankidroid:actionProviderClass="com.ichi2.ui.RtlCompliantActionProvider"
ankidroid:showAsAction="never"
android:visible="false"/>
<item
android:id="@+id/action_new_filtered_deck"
android:enabled="false"
android:menuCategory="secondary"
android:title="@string/new_dynamic_deck"/>
<item
android:id="@+id/action_check_database"
android:enabled="false"
android:menuCategory="secondary"
android:title="@string/check_db"/>
<item
android:id="@+id/action_check_media"
android:enabled="false"
android:menuCategory="secondary"
android:title="@string/check_media"/>
<item
android:id="@+id/action_empty_cards"
android:enabled="false"
android:menuCategory="secondary"
android:title="@string/empty_cards"/>
<item
android:id="@+id/action_restore_backup"
android:menuCategory="secondary"
android:title="@string/backup_restore"/>
<item
android:id="@+id/action_model_browser_open"
android:menuCategory="secondary"
android:title="@string/model_browser_label"/>
<item
android:id="@+id/action_import"
android:menuCategory="secondary"
android:title="@string/menu_import"/>
<item
android:id="@+id/action_export"
android:menuCategory="secondary"
android:title="@string/export_collection"/>
<group android:id="@+id/allItems">
<item
android:id="@+id/deck_picker_action_filter"
android:icon="@drawable/ic_search_white"
android:title="@string/search_decks"
ankidroid:actionViewClass="androidx.appcompat.widget.SearchView"
ankidroid:showAsAction="always|collapseActionView"/>
<!--
We want sync on the right, to be consistent with past version: #7737.
We're OK to do this as we're using "always" so it won't be hidden
-->
<item
android:id="@+id/action_sync"
android:icon="@drawable/ic_sync"
android:title="@string/button_sync"
ankidroid:showAsAction="always"/>
<item
android:id="@+id/action_undo"
android:title="@string/undo_action_review"
ankidroid:actionProviderClass="com.ichi2.ui.RtlCompliantActionProvider"
ankidroid:showAsAction="never"
android:visible="false"/>
<item
android:id="@+id/action_new_filtered_deck"
android:menuCategory="secondary"
android:title="@string/new_dynamic_deck"/>
<item
android:id="@+id/action_check_database"
android:menuCategory="secondary"
android:title="@string/check_db"/>
<item
android:id="@+id/action_check_media"
android:menuCategory="secondary"
android:title="@string/check_media"/>
<item
android:id="@+id/action_empty_cards"
android:menuCategory="secondary"
android:title="@string/empty_cards"/>
<item
android:id="@+id/action_restore_backup"
android:menuCategory="secondary"
android:title="@string/backup_restore"/>
<item
android:id="@+id/action_model_browser_open"
android:menuCategory="secondary"
android:title="@string/model_browser_label"/>
<item
android:id="@+id/action_import"
android:menuCategory="secondary"
android:title="@string/menu_import"/>
<item
android:id="@+id/action_export"
android:menuCategory="secondary"
android:title="@string/export_collection"/>
</group>
</menu>

View File

@ -17,7 +17,8 @@ import com.ichi2.testutils.BackupManagerTestUtilities
import com.ichi2.testutils.DbUtils
import com.ichi2.utils.KotlinCleanup
import com.ichi2.utils.ResourceLoader
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import net.ankiweb.rsdroid.BackendFactory
import org.apache.commons.exec.OS
import org.hamcrest.MatcherAssert.*
@ -35,6 +36,7 @@ import java.util.*
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(ParameterizedRobolectricTestRunner::class)
@KotlinCleanup("fix IDE lint issues")
@KotlinCleanup("replace `when` usages")
@ -319,18 +321,17 @@ class DeckPickerTest : RobolectricTest() {
@Test
@RunInBackground
fun doNotShowOptionsMenuWhenCollectionInaccessible() {
skipWindows()
fun doNotShowOptionsMenuWhenCollectionInaccessible() = runTest {
try {
enableNullCollection()
val d = super.startActivityNormallyOpenCollectionWithIntent(
DeckPickerEx::class.java, Intent()
)
runBlocking { d.createMenuJob?.join() }
d.updateMenuState()
assertThat(
"Options menu not displayed when collection is inaccessible",
d.optionsMenu?.hasVisibleItems(),
equalTo(false)
d.optionsMenuState,
equalTo(null)
)
} finally {
disableNullCollection()
@ -338,59 +339,17 @@ class DeckPickerTest : RobolectricTest() {
}
@Test
fun showOptionsMenuWhenCollectionAccessible() {
skipWindows()
fun showOptionsMenuWhenCollectionAccessible() = runTest {
try {
InitialActivityWithConflictTest.grantWritePermissions()
val d = super.startActivityNormallyOpenCollectionWithIntent(
DeckPickerEx::class.java, Intent()
)
runBlocking { d.createMenuJob?.join() }
d.updateMenuState()
assertThat(
"Options menu displayed when collection is accessible",
d.optionsMenu?.hasVisibleItems(),
equalTo(true)
)
} finally {
InitialActivityWithConflictTest.revokeWritePermissions()
}
}
@Test
@RunInBackground
fun doNotShowSyncBadgeWhenCollectionInaccessible() {
skipWindows()
try {
enableNullCollection()
val d = super.startActivityNormallyOpenCollectionWithIntent(
DeckPickerEx::class.java, Intent()
)
waitForAsyncTasksToComplete()
runBlocking { d.createMenuJob?.join() }
assertThat(
"Sync badge is not displayed when collection is inaccessible",
d.displaySyncBadge,
equalTo(false)
)
} finally {
disableNullCollection()
}
}
@Test
fun showSyncBadgeWhenCollectionAccessible() {
skipWindows()
try {
InitialActivityWithConflictTest.grantWritePermissions()
val d = super.startActivityNormallyOpenCollectionWithIntent(
DeckPickerEx::class.java, Intent()
)
waitForAsyncTasksToComplete()
runBlocking { d.createMenuJob?.join() }
assertThat(
"Sync badge is displayed when collection is accessible",
d.displaySyncBadge,
equalTo(true)
d.optionsMenuState,
`is`(notNullValue())
)
} finally {
InitialActivityWithConflictTest.revokeWritePermissions()
@ -619,7 +578,6 @@ class DeckPickerTest : RobolectricTest() {
var databaseErrorDialog = 0
var displayedAnalyticsOptIn = false
var optionsMenu: Menu? = null
var displaySyncBadge = false
override fun showDatabaseErrorDialog(id: Int) {
databaseErrorDialog = id
@ -642,10 +600,5 @@ class DeckPickerTest : RobolectricTest() {
optionsMenu = menu
return super.onPrepareOptionsMenu(menu)
}
override suspend fun displaySyncBadge(menu: Menu) {
displaySyncBadge = true
super.displaySyncBadge(menu)
}
}
}

View File

@ -55,7 +55,6 @@ import net.ankiweb.rsdroid.testing.RustBackendLoader
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.hamcrest.Matchers.equalTo
import org.junit.*
import org.robolectric.Robolectric
import org.robolectric.Shadows
@ -426,12 +425,6 @@ open class RobolectricTest : CollectionGetter {
col
}
/** The coroutine implemention on Windows/Robolectric seems to inexplicably hang sometimes */
fun skipWindows() {
val name = System.getProperty("os.name") ?: ""
assumeThat(name.startsWith("Windows"), equalTo(false))
}
@Throws(ConfirmModSchemaException::class)
protected fun upgradeToSchedV2(): SchedV2 {
col.changeSchedulerVer(2)

View File

@ -22,24 +22,30 @@ import androidx.test.core.app.ActivityScenario
import com.afollestad.materialdialogs.WhichButton
import com.afollestad.materialdialogs.actions.getActionButton
import com.afollestad.materialdialogs.input.getInputField
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.DeckPicker
import com.ichi2.anki.R
import com.ichi2.anki.RobolectricTest
import com.ichi2.libanki.DeckManager
import com.ichi2.libanki.backend.exception.DeckRenameException
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class CreateDeckDialogTest : RobolectricTest() {
private var mActivityScenario: ActivityScenario<DeckPicker>? = null
@ -149,8 +155,13 @@ class CreateDeckDialogTest : RobolectricTest() {
}
@Test
@Ignore("this is difficult to test at the moment")
fun searchDecksIconVisibilityDeckCreationTest() {
skipWindows()
// this is currently broken, as it has a few issues:
// - we need to await the completion of createMenuJob, as the menu is created asynchronously
// - the calls to `decks` should be made using withCol, and this routine should be asynchronous
// - when I attempted to implement this, I found the test hung. I'm guessing it might be some
// sort of deadlock, where a runBlocking() call is waiting for some UI state to update
mActivityScenario!!.onActivity { deckPicker ->
val decks = deckPicker.col.decks
val deckCounter = AtomicInteger(1)
@ -186,59 +197,21 @@ class CreateDeckDialogTest : RobolectricTest() {
// immediately so that the test can continue
runBlocking {
deckPicker.createMenuJob?.join()
deckPicker.updateSearchDecksIconVisibility()
}
}
@Test
fun searchDecksIconVisibilitySubdeckCreationTest() {
skipWindows()
mActivityScenario!!.onActivity { deckPicker ->
var createDeckDialog = CreateDeckDialog(deckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.DECK, null)
val decks = deckPicker.col.decks
createDeckDialog.setOnNewDeckCreated {
assertEquals(10, decks.count())
updateSearchDecksIcon(deckPicker)
assertTrue(deckPicker.searchDecksIcon!!.isVisible)
awaitJob(deckPicker.confirmDeckDeletion(decks.id("Deck0::Deck1")))
assertEquals(2, decks.count())
updateSearchDecksIcon(deckPicker)
assertFalse(deckPicker.searchDecksIcon!!.isVisible)
}
createDeckDialog.createDeck(deckTreeName(0, 8, "Deck"))
createDeckDialog = CreateDeckDialog(deckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.DECK, null)
createDeckDialog.setOnNewDeckCreated {
assertEquals(12, decks.count())
updateSearchDecksIcon(deckPicker)
assertTrue(deckPicker.searchDecksIcon!!.isVisible)
awaitJob(deckPicker.confirmDeckDeletion(decks.id("Deck0::Deck1")))
assertEquals(2, decks.count())
updateSearchDecksIcon(deckPicker)
assertFalse(deckPicker.searchDecksIcon!!.isVisible)
}
createDeckDialog.createDeck(deckTreeName(0, 10, "Deck"))
createDeckDialog = CreateDeckDialog(deckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.DECK, null)
createDeckDialog.setOnNewDeckCreated {
assertEquals(6, decks.count())
updateSearchDecksIcon(deckPicker)
assertFalse(deckPicker.searchDecksIcon!!.isVisible)
}
createDeckDialog.createDeck(deckTreeName(0, 4, "Deck"))
createDeckDialog = CreateDeckDialog(deckPicker, R.string.new_deck, CreateDeckDialog.DeckDialogType.DECK, null)
createDeckDialog.setOnNewDeckCreated {
assertEquals(12, decks.count())
updateSearchDecksIcon(deckPicker)
assertTrue(deckPicker.searchDecksIcon!!.isVisible)
}
createDeckDialog.createDeck(deckTreeName(6, 11, "Deck"))
fun searchDecksIconVisibilitySubdeckCreationTest() = runTest {
val deckPicker =
suspendCoroutine { coro -> mActivityScenario!!.onActivity { coro.resume(it) } }
deckPicker.updateMenuState()
assertEquals(deckPicker.optionsMenuState!!.searchIcon, false)
// a single top-level deck with lots of subdecks should turn the icon on
withCol {
decks.id(deckTreeName(0, 10, "Deck"))
}
deckPicker.updateMenuState()
assertEquals(deckPicker.optionsMenuState!!.searchIcon, true)
}
private fun deckTreeName(start: Int, end: Int, prefix: String): String {