Skip to content

Commit e9928af

Browse files
NF: Adding David's tests.
This test ensures that every changes possibly done in the deck options are correctly applied
1 parent 085f374 commit e9928af

File tree

5 files changed

+206
-9
lines changed

5 files changed

+206
-9
lines changed

AnkiDroid/src/main/java/com/ichi2/anki/FilteredDeckOptions.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
package com.ichi2.anki
2121

22+
import android.content.Context
23+
import android.content.Intent
2224
import android.content.SharedPreferences
2325
import android.os.Bundle
2426
import android.preference.CheckBoxPreference
@@ -35,6 +37,7 @@ import com.ichi2.libanki.Deck.Companion.PREVIEW_AGAIN_SECS
3537
import com.ichi2.libanki.Deck.Companion.PREVIEW_GOOD_SECS
3638
import com.ichi2.libanki.Deck.Companion.PREVIEW_HARD_SECS
3739
import com.ichi2.libanki.Deck.Companion.RESCHED
40+
import com.ichi2.libanki.DeckId
3841
import com.ichi2.libanki.FilteredDeck
3942
import com.ichi2.libanki.FilteredDeck.Term
4043
import com.ichi2.preferences.StepsPreference.Companion.convertFromJSON
@@ -53,6 +56,8 @@ class FilteredDeckOptions :
5356
val filteredDeck: FilteredDeck
5457
get() = super.deck as FilteredDeck
5558

59+
lateinit var secondFilterSign: CheckBoxPreference
60+
5661
companion object {
5762
const val SEARCH = "search"
5863
const val LIMIT = "limit"
@@ -63,6 +68,14 @@ class FilteredDeckOptions :
6368
const val STEPS = "steps"
6469
const val STEPS_ON = "stepsOn"
6570
const val PRESET = "preset"
71+
72+
fun createIntent(
73+
context: Context,
74+
did: DeckId,
75+
): Intent =
76+
Intent(context, FilteredDeckOptions::class.java).apply {
77+
putExtra("did", did)
78+
}
6679
}
6780

6881
// TODO: not anymore used in libanki?
@@ -130,9 +143,8 @@ class FilteredDeckOptions :
130143
SEARCH -> {
131144
filteredDeck.firstFilter.search = value as String
132145
}
133-
134146
LIMIT -> {
135-
filteredDeck.firstFilter.limit = value as Int
147+
filteredDeck.firstFilter.limit = (value as String).toInt()
136148
}
137149
ORDER -> {
138150
filteredDeck.firstFilter.order = (value as String).toInt()
@@ -161,7 +173,7 @@ class FilteredDeckOptions :
161173
}
162174
}
163175
STEPS -> {
164-
filteredDeck.delays = convertToJSON((value as String))
176+
filteredDeck.delays = convertToJSON(value as String)
165177
}
166178
PRESET -> {
167179
val i: Int = (value as String).toInt()
@@ -230,8 +242,10 @@ class FilteredDeckOptions :
230242
deck = if (extras?.containsKey("did") == true) {
231243
col.decks.get(extras.getLong("did"))
232244
} else {
245+
Timber.d("no deckId supplied. Using current deck")
233246
null
234247
} ?: col.decks.current()
248+
Timber.i("opened for deck %d", deck.id)
235249
registerExternalStorageListener()
236250
if (deck.isRegular) {
237251
Timber.w("Deck is not a dyn deck")
@@ -360,7 +374,7 @@ class FilteredDeckOptions :
360374

361375
@Suppress("deprecation")
362376
private fun setupSecondFilterListener() {
363-
val secondFilterSign = findPreference("filterSecond") as CheckBoxPreference
377+
secondFilterSign = findPreference("filterSecond") as CheckBoxPreference
364378
val secondFilter = findPreference("secondFilter") as PreferenceCategory
365379
if (pref.hasSecondFilter) {
366380
secondFilter.isEnabled = true

AnkiDroid/src/main/java/com/ichi2/libanki/FilteredDeck.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,6 @@ value class FilteredDeck(
100100
set(value) {
101101
jsonObject.put(TERMS, value)
102102
}
103+
104+
override fun toString(): String = jsonObject.toString()
103105
}

AnkiDroid/src/main/java/com/ichi2/ui/AppCompatPreferenceActivity.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import android.view.MenuItem
3434
import android.view.View
3535
import android.view.ViewGroup
3636
import androidx.annotation.LayoutRes
37+
import androidx.annotation.VisibleForTesting
3738
import androidx.appcompat.app.ActionBar
3839
import androidx.appcompat.app.AlertDialog
3940
import androidx.appcompat.app.AppCompatDelegate
@@ -79,7 +80,9 @@ abstract class AppCompatPreferenceActivity<PreferenceHack : AppCompatPreferenceA
7980
private lateinit var unmountReceiver: BroadcastReceiver
8081
protected lateinit var col: Collection
8182
private set
82-
protected lateinit var pref: PreferenceHack
83+
84+
@VisibleForTesting
85+
internal lateinit var pref: PreferenceHack
8386

8487
// value class can't be lateinit.
8588
// Instead we use a backing field.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright (c) 2025 David Allison <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
17+
package com.ichi2.anki
18+
19+
import androidx.core.content.edit
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import com.ichi2.anki.FilteredDeckOptions.Companion.LIMIT
22+
import com.ichi2.anki.FilteredDeckOptions.Companion.LIMIT_2
23+
import com.ichi2.anki.FilteredDeckOptions.Companion.ORDER
24+
import com.ichi2.anki.FilteredDeckOptions.Companion.ORDER_2
25+
import com.ichi2.anki.FilteredDeckOptions.Companion.SEARCH
26+
import com.ichi2.anki.FilteredDeckOptions.Companion.SEARCH_2
27+
import com.ichi2.anki.FilteredDeckOptions.Companion.STEPS
28+
import com.ichi2.anki.FilteredDeckOptions.Companion.STEPS_ON
29+
import com.ichi2.anki.FlashCardsContract.Note.MOD
30+
import com.ichi2.anki.FlashCardsContract.Note.USN
31+
import com.ichi2.libanki.Consts
32+
import com.ichi2.libanki.Deck.Companion.BROWSER_COLLAPSED
33+
import com.ichi2.libanki.Deck.Companion.COLLAPSED
34+
import com.ichi2.libanki.Deck.Companion.DELAYS
35+
import com.ichi2.libanki.Deck.Companion.DESCRIPTION
36+
import com.ichi2.libanki.Deck.Companion.DYN
37+
import com.ichi2.libanki.Deck.Companion.ID
38+
import com.ichi2.libanki.Deck.Companion.NAME
39+
import com.ichi2.libanki.Deck.Companion.PREVIEW_AGAIN_SECS
40+
import com.ichi2.libanki.Deck.Companion.PREVIEW_GOOD_SECS
41+
import com.ichi2.libanki.Deck.Companion.PREVIEW_HARD_SECS
42+
import com.ichi2.libanki.Deck.Companion.RESCHED
43+
import com.ichi2.libanki.DeckId
44+
import com.ichi2.libanki.FilteredDeck
45+
import com.ichi2.libanki.FilteredDeck.Companion.TERMS
46+
import com.ichi2.testutils.isJsonHolderEqual
47+
import org.hamcrest.MatcherAssert.assertThat
48+
import org.hamcrest.Matchers.equalTo
49+
import org.hamcrest.Matchers.not
50+
import org.intellij.lang.annotations.Language
51+
import org.json.JSONArray
52+
import org.json.JSONObject
53+
import org.junit.Test
54+
import org.junit.runner.RunWith
55+
56+
@RunWith(AndroidJUnit4::class)
57+
class FilteredDeckOptionsTest : RobolectricTest() {
58+
companion object {
59+
const val LRN_TODAY = "lrnToday"
60+
const val REV_TODAY = "revToday"
61+
const val NEW_TODAY = "newToday"
62+
const val TIME_TODAY = "timeToday"
63+
const val SEPARATE = "separate"
64+
const val PREVIEW_DELAY = "previewDelay"
65+
}
66+
67+
@Test
68+
fun `integration test`() {
69+
@Language("JSON")
70+
val expected =
71+
FilteredDeck(
72+
"""{
73+
"$ID" : 1737164378146,
74+
"$MOD" : 1737164378,
75+
"$NAME" : "Filtered",
76+
"$USN" : -1,
77+
"$LRN_TODAY" : [0,0],
78+
"$REV_TODAY" : [0,0],
79+
"$NEW_TODAY" : [0,0],
80+
"$TIME_TODAY" : [0,0],
81+
"$COLLAPSED" : false,
82+
"$BROWSER_COLLAPSED" : false,
83+
"$DESCRIPTION" : "",
84+
"$DYN" : 1,
85+
"$RESCHED" : true,
86+
"$TERMS" : [["", 100, 0]],
87+
"$SEPARATE" : true,
88+
"$DELAYS" : null,
89+
"$PREVIEW_DELAY" : 0,
90+
"$PREVIEW_AGAIN_SECS" : 60,
91+
"$PREVIEW_HARD_SECS" : 600,
92+
"$PREVIEW_GOOD_SECS" : 0
93+
}""",
94+
)
95+
assertThat("should not be using default deck", filteredDeckConfig.id, not(equalTo(Consts.DEFAULT_DECK_ID)))
96+
assertThat("before", filteredDeckConfig.removeNonDeterministicValues(), isJsonHolderEqual(expected.removeNonDeterministicValues()))
97+
98+
withFilteredDeckOptions(newFilteredDeckId) {
99+
@Suppress("DEPRECATION")
100+
secondFilterSign.onPreferenceChangeListener.onPreferenceChange(null, true)
101+
pref.edit(commit = true) {
102+
putString(SEARCH_2, "search_2")
103+
putString(LIMIT_2, "42")
104+
putString(ORDER_2, "43")
105+
putString(SEARCH, "search_1")
106+
putString(LIMIT, "44")
107+
putString(ORDER, "45")
108+
putBoolean(RESCHED, false)
109+
putInt(PREVIEW_AGAIN_SECS, 46)
110+
putInt(PREVIEW_HARD_SECS, 47)
111+
putInt(PREVIEW_GOOD_SECS, 48)
112+
putBoolean(STEPS_ON, true)
113+
putString(STEPS, "50 51 52")
114+
// TODO: Create a test for PRESET
115+
}
116+
}
117+
118+
val updatedExpectation =
119+
expected
120+
.copyWith {
121+
val firstFilter = JSONArray(listOf("search_1", 44, 45))
122+
val secondFilter = JSONArray(listOf("search_2", 42, 43)) //
123+
it.put(TERMS, JSONArray(listOf(firstFilter, secondFilter)))
124+
it.put(RESCHED, false)
125+
it.put(PREVIEW_AGAIN_SECS, 46)
126+
it.put(PREVIEW_HARD_SECS, 47)
127+
it.put(PREVIEW_GOOD_SECS, 48)
128+
it.put(DELAYS, JSONArray(listOf(50, 51, 52)))
129+
}
130+
assertThat(
131+
"after",
132+
filteredDeckConfig.removeNonDeterministicValues(),
133+
isJsonHolderEqual(updatedExpectation.removeNonDeterministicValues()),
134+
)
135+
}
136+
137+
/**
138+
* A copy of [this] without its "id" and "mod" keys.
139+
* Those two keys are the only non deterministic values, removing them allows to compare the returned value to some expected deck.
140+
*/
141+
fun FilteredDeck.removeNonDeterministicValues() =
142+
this.copyWith { copy ->
143+
copy.remove("id")
144+
copy.remove("mod")
145+
}
146+
147+
/**
148+
* The result of applying [block] to [this], leaving the input unchanged.
149+
*/
150+
fun FilteredDeck.copyWith(block: (JSONObject) -> Unit) =
151+
FilteredDeck(this.toString()).apply {
152+
block(jsonObject)
153+
}
154+
155+
private fun withFilteredDeckOptions(
156+
deckId: DeckId,
157+
block: FilteredDeckOptions.() -> Unit,
158+
) {
159+
startRegularActivity<FilteredDeckOptions>(FilteredDeckOptions.createIntent(targetContext, deckId)).apply(block)
160+
}
161+
162+
/**
163+
* A filtered deck named "Filtered" with default config, always the same deck during a test.
164+
*/
165+
private val filteredDeckConfig
166+
get() =
167+
newFilteredDeckId.let { did ->
168+
col.decks.get(did) as FilteredDeck
169+
}
170+
171+
/**
172+
* The deck id of a fresh filtered deck. The deck is created the first time this value is accessed, the id is then constant.*/
173+
private val newFilteredDeckId by lazy { col.decks.newFiltered("Filtered") }
174+
175+
private val defaultDeckConfig
176+
get() = col.decks.configDictForDeckId(Consts.DEFAULT_DECK_ID)
177+
}

AnkiDroid/src/test/java/com/ichi2/anki/RobolectricTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.ichi2.anki
1818

1919
import android.Manifest
20+
import android.app.Activity
2021
import android.app.Application
2122
import android.content.Context
2223
import android.content.Intent
@@ -294,7 +295,7 @@ open class RobolectricTest :
294295
}
295296

296297
@JvmStatic // Using protected members which are not @JvmStatic in the superclass companion is unsupported yet
297-
protected fun <T : AnkiActivity?> startActivityNormallyOpenCollectionWithIntent(
298+
protected fun <T : Activity?> startActivityNormallyOpenCollectionWithIntent(
298299
testClass: RobolectricTest,
299300
clazz: Class<T>?,
300301
i: Intent?,
@@ -368,14 +369,14 @@ open class RobolectricTest :
368369
return collectionModels.byName(noteTypeName)!!.deepClone()
369370
}
370371

371-
internal fun <T : AnkiActivity?> startActivityNormallyOpenCollectionWithIntent(
372+
internal fun <T : Activity?> startActivityNormallyOpenCollectionWithIntent(
372373
clazz: Class<T>?,
373374
i: Intent?,
374375
): T = startActivityNormallyOpenCollectionWithIntent(this, clazz, i)
375376

376-
internal inline fun <reified T : AnkiActivity?> startRegularActivity(): T = startRegularActivity(null)
377+
internal inline fun <reified T : Activity?> startRegularActivity(): T = startRegularActivity(null)
377378

378-
internal inline fun <reified T : AnkiActivity?> startRegularActivity(i: Intent? = null): T =
379+
internal inline fun <reified T : Activity?> startRegularActivity(i: Intent? = null): T =
379380
startActivityNormallyOpenCollectionWithIntent(T::class.java, i)
380381

381382
/**

0 commit comments

Comments
 (0)