Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: start Reviewer port #16455

Merged
merged 5 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions AnkiDroid/src/androidTest/java/com/ichi2/anki/ReviewerFragmentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* This program is free software; you can redistribute it and/or modify it under
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No copyright?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's basically a copy of ReviewerTest, which doesn't have an author in the copyright header, and I didn't want to license it all under myself

* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.ichi2.anki

import androidx.core.content.edit
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.preferences.sharedPrefs
import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.testutil.GrantStoragePermission.storagePermission
import com.ichi2.anki.testutil.grantPermissions
import com.ichi2.anki.testutil.notificationPermission
import com.ichi2.libanki.Collection
import com.ichi2.testutils.Flaky
import com.ichi2.testutils.OS
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit

@RunWith(AndroidJUnit4::class)
class ReviewerFragmentTest : InstrumentedTest() {

// Launch IntroductionActivity instead of DeckPicker activity because in CI
// builds, it seems to create IntroductionActivity after the DeckPicker,
// causing the DeckPicker activity to be destroyed. As a consequence, this
// will throw RootViewWithoutFocusException when Espresso tries to interact
// with an already destroyed activity. By launching IntroductionActivity, we
// ensure that IntroductionActivity is launched first and navigate to the
// DeckPicker -> Reviewer activities
@get:Rule
val activityScenarioRule = ActivityScenarioRule(IntroductionActivity::class.java)

@get:Rule
val runtimePermissionRule = grantPermissions(storagePermission, notificationPermission)

@Test
@Flaky(os = OS.ALL, "Fails on CI with timing issues frequently")
fun testCustomSchedulerWithCustomData() {
setNewReviewer()
col.cardStateCustomizer =
"""
states.good.normal.review.easeFactor = 3.0;
states.good.normal.review.scheduledDays = 123;
customData.good.c += 1;
"""
val note = addNoteUsingBasicModel("foo", "bar")
val card = note.firstCard(col)
val deck = col.decks.get(note.notetype.did)!!
card.moveToReviewQueue()
col.backend.updateCards(
listOf(
card.toBackendCard().toBuilder().setCustomData("""{"c":1}""").build()
),
true
)

closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName(deck.name)

var cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(card.factor))
assertThat(cardFromDb.interval, equalTo(card.ivl))
assertThat(cardFromDb.customData, equalTo("""{"c":1}"""))

clickShowAnswerAndAnswerGood()

cardFromDb = col.getCard(card.id).toBackendCard()
assertThat(cardFromDb.easeFactor, equalTo(3000))
assertThat(cardFromDb.interval, equalTo(123))
assertThat(cardFromDb.customData, equalTo("""{"c":2}"""))
}

@Test
@Flaky(os = OS.ALL, "Fails on CI with timing issues frequently")
fun testCustomSchedulerWithRuntimeError() {
setNewReviewer()
// Issue 15035 - runtime errors weren't handled
col.cardStateCustomizer = "states.this_is_not_defined.normal.review = 12;"
addNoteUsingBasicModel()

closeGetStartedScreenIfExists()
closeBackupCollectionDialogIfExists()
reviewDeckWithName("Default")

clickShowAnswer()

ensureAnswerButtonsAreDisplayed()
}

private fun closeGetStartedScreenIfExists() {
onView(withId(R.id.get_started)).withFailureHandler { _, _ -> }.perform(click())
}

private fun closeBackupCollectionDialogIfExists() {
onView(withText(R.string.button_backup_later))
.withFailureHandler { _, _ -> }
.perform(click())
}

private fun clickOnDeckWithName(deckName: String) {
onView(withId(R.id.files)).checkWithTimeout(matches(hasDescendant(withText(deckName))))
onView(withId(R.id.files)).perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(withText(deckName)),
click()
)
)
}

private fun clickOnStudyButtonIfExists() {
onView(withId(R.id.studyoptions_start))
.withFailureHandler { _, _ -> }
.perform(click())
}

private fun reviewDeckWithName(deckName: String) {
clickOnDeckWithName(deckName)
// Adding cards directly to the database while in the Deck Picker screen
// will not update the page with correct card counts. Hence, clicking
// on the deck will bring us to the study options page where we need to
// click on the Study button. If we have added cards to the database
// before the Deck Picker screen has fully loaded, then we skip clicking
// the Study button
clickOnStudyButtonIfExists()
}

private fun clickShowAnswerAndAnswerGood() {
clickShowAnswer()
ensureAnswerButtonsAreDisplayed()
onView(withId(R.id.good_button)).perform(click())
}

private fun clickShowAnswer() {
onView(withId(R.id.show_answer)).perform(click())
}

private fun ensureAnswerButtonsAreDisplayed() {
// We need to wait for the card to fully load to allow enough time for
// the messages to be passed in and out of the WebView when evaluating
// the custom JS scheduler code. The ease buttons are hidden until the
// custom scheduler has finished running
onView(withId(R.id.good_button)).checkWithTimeout(
matches(isDisplayed()),
100,
// Increase to a max of 30 seconds because CI builds can be very
// slow
TimeUnit.SECONDS.toMillis(30)
)
}

private fun setNewReviewer() {
testContext.sharedPrefs().edit {
putBoolean("newReviewer", true)
}
}
}

private var Collection.cardStateCustomizer: String?
get() = config.get("cardStateCustomizer")
set(value) { config.set("cardStateCustomizer", value) }
9 changes: 7 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import com.ichi2.anki.snackbar.BaseSnackbarBuilderProvider
import com.ichi2.anki.snackbar.SnackbarBuilder
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.ui.dialogs.storageMigrationFailedDialogIsShownOrPending
import com.ichi2.anki.ui.windows.reviewer.ReviewerFragment
import com.ichi2.anki.utils.SECONDS_PER_DAY
import com.ichi2.anki.widgets.DeckAdapter
import com.ichi2.anki.worker.SyncMediaWorker
Expand Down Expand Up @@ -2305,8 +2306,12 @@ open class DeckPicker :
}

private fun openReviewer() {
val reviewer = Intent(this, Reviewer::class.java)
reviewLauncher.launch(reviewer)
val intent = if (sharedPrefs().getBoolean("newReviewer", false)) {
ReviewerFragment.getIntent(this)
} else {
Intent(this, Reviewer::class.java)
}
reviewLauncher.launch(intent)
}

override fun onCreateCustomStudySession() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.ichi2.anki.previewer

import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.view.View
Expand All @@ -40,7 +41,6 @@ import com.ichi2.anki.R
import com.ichi2.anki.ViewerResourceHandler
import com.ichi2.anki.dialogs.TtsVoicesDialogFragment
import com.ichi2.anki.localizedErrorMessage
import com.ichi2.anki.pages.AnkiServer
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.utils.ext.packageManager
import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat
Expand Down Expand Up @@ -89,7 +89,7 @@ abstract class CardViewerFragment(@LayoutRes layout: Int) : Fragment(layout) {
}

loadDataWithBaseURL(
"http://${AnkiServer.LOCALHOST}/",
viewModel.baseUrl(),
stdHtml(requireContext(), Themes.currentTheme.isNightMode),
"text/html",
null,
Expand Down Expand Up @@ -134,6 +134,12 @@ abstract class CardViewerFragment(@LayoutRes layout: Int) : Fragment(layout) {
return resourceHandler.shouldInterceptRequest(request)
}

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
// TODO remove this after the backend is upgraded to v0.1.39
view?.evaluateJavascript("globalThis.ankidroid = globalThis.ankidroid || {}; ankidroid.postBaseUrl = ``", null)
}

override fun onPageFinished(view: WebView?, url: String?) {
viewModel.onPageFinished(isAfterRecreation = savedInstanceState != null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.ichi2.anki.cardviewer.MediaErrorHandler
import com.ichi2.anki.cardviewer.SoundErrorBehavior
import com.ichi2.anki.cardviewer.SoundErrorListener
import com.ichi2.anki.launchCatchingIO
import com.ichi2.anki.pages.AnkiServer
import com.ichi2.libanki.Card
import com.ichi2.libanki.Sound
import com.ichi2.libanki.TtsPlayer
Expand Down Expand Up @@ -78,6 +79,8 @@ abstract class CardViewerViewModel(
*/
abstract fun onPageFinished(isAfterRecreation: Boolean)

open fun baseUrl(): String = "http://${AnkiServer.LOCALHOST}/"

fun setSoundPlayerEnabled(isEnabled: Boolean) {
cardMediaPlayer.isEnabled = isEnabled
}
Expand Down
Loading