Sitelet https://github.com/ankidroid/Anki-Android/commit/88e0a276a50775a177af8da939aaeea36f556830
Skip to content

Commit 88e0a27

Browse files
committed
Add test for ReviewerControlPreference.getPreferenceAssignedTo
Covers side-specific binding lookup: a binding on QUESTION/ANSWER only matches a preference on the same side, while a binding on BOTH conflicts with any single-side binding. Verified via mutation testing that re-introducing the bug (ignoring the card side) breaks the test. Fixes part of #13283
1 parent eb22e1e commit 88e0a27

2 files changed

Lines changed: 111 additions & 1 deletion

File tree

‎AnkiDroid/src/main/java/com/ichi2/preferences/ReviewerControlPreference.kt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ open class ReviewerControlPreference : ControlPreference {
8484
it::class == ReviewerControlPreference::class
8585
} as List<ReviewerControlPreference>
8686

87-
@NeedsTest("Ensure correct preference is returned for side-specific binding")
8887
override fun getPreferenceAssignedTo(binding: Binding): ControlPreference? {
8988
val cardSide = side ?: return super.getPreferenceAssignedTo(binding)
9089
val reviewerBinding = ReviewerBinding(binding, cardSide)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
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.preferences
18+
19+
import androidx.preference.PreferenceManager
20+
import androidx.test.ext.junit.runners.AndroidJUnit4
21+
import com.ichi2.anki.RobolectricTest
22+
import com.ichi2.anki.cardviewer.Gesture
23+
import com.ichi2.anki.reviewer.Binding
24+
import com.ichi2.anki.reviewer.CardSide
25+
import com.ichi2.anki.reviewer.MappableBinding.Companion.toPreferenceString
26+
import com.ichi2.anki.reviewer.ReviewerBinding
27+
import com.ichi2.testutils.getJavaMethodAsAccessible
28+
import org.junit.Test
29+
import org.junit.runner.RunWith
30+
import kotlin.test.assertEquals
31+
import kotlin.test.assertNull
32+
33+
/**
34+
* Test of [ReviewerControlPreference.getPreferenceAssignedTo]
35+
*
36+
* `getRelatedPreferences` matches preferences by exact class (`it::class == ReviewerControlPreference::class`),
37+
* so instances here must be real [ReviewerControlPreference]s rather than a test subclass - `side` and
38+
* `getPreferenceAssignedTo` are accessed via reflection as they're `protected`.
39+
*/
40+
@RunWith(AndroidJUnit4::class)
41+
class ReviewerControlPreferenceTest : RobolectricTest() {
42+
private val bindingsToPersist = mutableMapOf<ReviewerControlPreference, Binding>()
43+
44+
@Test
45+
fun `binding on the same side is found`() {
46+
val gesture = Binding.GestureInput(Gesture.SWIPE_UP)
47+
val questionPref = buildPreference(key = "question", side = CardSide.QUESTION, binding = gesture)
48+
buildScreen(questionPref)
49+
50+
assertEquals(questionPref, questionPref.callGetPreferenceAssignedTo(gesture))
51+
}
52+
53+
@Test
54+
fun `binding on a different side is not found`() {
55+
val gesture = Binding.GestureInput(Gesture.SWIPE_UP)
56+
val questionPref = buildPreference(key = "question", side = CardSide.QUESTION, binding = gesture)
57+
val answerPref = buildPreference(key = "answer", side = CardSide.ANSWER)
58+
buildScreen(questionPref, answerPref)
59+
60+
assertNull(answerPref.callGetPreferenceAssignedTo(gesture))
61+
}
62+
63+
@Test
64+
fun `binding on 'both' sides conflicts with a single-side binding`() {
65+
val gesture = Binding.GestureInput(Gesture.SWIPE_UP)
66+
val questionPref = buildPreference(key = "question", side = CardSide.QUESTION, binding = gesture)
67+
val bothPref = buildPreference(key = "both", side = CardSide.BOTH)
68+
buildScreen(questionPref, bothPref)
69+
70+
assertEquals(questionPref, bothPref.callGetPreferenceAssignedTo(gesture))
71+
}
72+
73+
private fun buildPreference(
74+
key: String,
75+
side: CardSide,
76+
binding: Binding? = null,
77+
): ReviewerControlPreference {
78+
val pref = ReviewerControlPreference(targetContext)
79+
pref.key = key
80+
pref.setSideForTest(side)
81+
if (binding != null) bindingsToPersist[pref] = binding
82+
return pref
83+
}
84+
85+
private fun buildScreen(vararg preferences: ReviewerControlPreference) {
86+
val preferenceManager = PreferenceManager(targetContext)
87+
val screen = preferenceManager.createPreferenceScreen(targetContext)
88+
preferenceManager.setPreferences(screen)
89+
preferences.forEach { screen.addPreference(it) }
90+
preferences.forEach { pref ->
91+
bindingsToPersist[pref]?.let { binding ->
92+
val side = requireNotNull(pref.getSideForTest())
93+
pref.value = listOf(ReviewerBinding(binding, side)).toPreferenceString()
94+
}
95+
}
96+
}
97+
98+
private fun ReviewerControlPreference.sideField() =
99+
ReviewerControlPreference::class.java.getDeclaredField("side").apply { isAccessible = true }
100+
101+
private fun ReviewerControlPreference.setSideForTest(side: CardSide?) = sideField().set(this, side)
102+
103+
private fun ReviewerControlPreference.getSideForTest() = sideField().get(this) as CardSide?
104+
105+
private fun ReviewerControlPreference.callGetPreferenceAssignedTo(binding: Binding): ControlPreference? =
106+
getJavaMethodAsAccessible(
107+
ReviewerControlPreference::class.java,
108+
"getPreferenceAssignedTo",
109+
Binding::class.java,
110+
).invoke(this, binding) as ControlPreference?
111+
}

0 commit comments

Comments
 (0)