|
| 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 | +} |
0 commit comments