Skip to content

Commit

Permalink
test(deck-options): discard dialog if changes made
Browse files Browse the repository at this point in the history
Issue 14438

----

This was a difficult change to make:

* timing considerations on WebView callbacks

I spent time time trying to use `onView` inside `onActivity`.

After research, `onView` should be outside the `onActivity`
hook: https://stackoverflow.com/a/64284563
  • Loading branch information
david-allison committed Jan 22, 2025
1 parent d309d2a commit 6f5452b
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
package com.ichi2.anki.pages

import androidx.lifecycle.Lifecycle.State
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
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.R
Expand All @@ -25,9 +31,11 @@ import com.ichi2.anki.tests.InstrumentedTest
import com.ichi2.anki.testutil.findFragmentById
import com.ichi2.libanki.Consts
import com.ichi2.testutils.common.assertTrueWithTimeout
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import timber.log.Timber
import kotlin.test.assertEquals

/** Tests [DeckOptions] */
Expand All @@ -42,8 +50,29 @@ class DeckOptionsTest : InstrumentedTest() {
@Test
fun discardChangesIsNotShownIfNoChanges() = testDeckOptions { assertBackPressClosesOptions() }

@Test
fun discardChangesIsShownIfChangeMade() {
testDeckOptions { makeChange() }

Espresso.pressBack()

onView(withText("Discard current input?"))
.inRoot(isDialog())
.check(matches(isEnabled()))
}

@Test
@Ignore("broken on main")
fun discardChangesIsNotShownIfChangeIsReversed() =
testDeckOptions {
makeChangeAndUndo()
assertBackPressClosesOptions()
}

/**
* Runs [block] with [DeckOptions] as the receiver. Intended for tests
* Runs [block] with [DeckOptions] as the receiver. Intended for test setup
*
* `onView` should not be called inside this method, instead call it afterwards
*/
private fun testDeckOptions(block: (DeckOptions).() -> Unit) {
assertEquals(State.RESUMED, activityRule.scenario.state)
Expand All @@ -59,3 +88,36 @@ private fun DeckOptions.assertBackPressClosesOptions() {
// we assert this as [actuallyClose] launches a task
assertTrueWithTimeout("fragment closing") { isClosingFragment }
}

/** Changes an option, so there are unsaved changes */
private fun DeckOptions.makeChange() {
toggleFsrs()
}

/** Changes an option, and undoes the change so there are no unsaved changes */
private fun DeckOptions.makeChangeAndUndo() {
toggleFsrs()
toggleFsrs()
}

private fun DeckOptions.toggleFsrs() {
val js =
"""
// Find the FSRS heading. This is not translated. Exclude the FSRS modal.
const element = Array.from(document.getElementsByTagName("h1")).filter(x => x.innerText == "FSRS" && !x.classList.contains("modal-title"))[0];
// Find the 'FSRS' container
const container = element.closest(".container");
// Find the only checkbox, and click it
container.querySelectorAll('input[type="checkbox"]')[0].click()
""".trimIndent()

fun execToggleFsrsJs() = this.webView.evaluateJavascript(js) {}

if (pageWebViewClient.callbacksExecuted) {
Timber.v("scheduling JS callback: 'toggleFsrs'")
execToggleFsrsJs()
} else {
Timber.v("scheduling JS callback: 'toggleFsrs'")
pageWebViewClient.onPageFinishedCallbacks.add { execToggleFsrsJs() }
}
}
6 changes: 5 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.CallSuper
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.google.android.material.appbar.MaterialToolbar
Expand All @@ -46,6 +47,9 @@ open class PageFragment(
lateinit var webView: WebView
private val server = AnkiServer(this).also { it.start() }

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
lateinit var pageWebViewClient: PageWebViewClient

/**
* Override this to set a custom [WebViewClient] to the page.
* This is called in [onViewCreated].
Expand Down Expand Up @@ -93,7 +97,7 @@ open class PageFragment(
view: View,
savedInstanceState: Bundle?,
) {
val pageWebViewClient = onCreateWebViewClient(savedInstanceState)
pageWebViewClient = onCreateWebViewClient(savedInstanceState)
webView =
view.findViewById<WebView>(R.id.webview).apply {
with(settings) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import com.google.android.material.color.MaterialColors
import com.ichi2.anki.OnPageFinishedCallback
Expand All @@ -39,6 +40,9 @@ open class PageWebViewClient : WebViewClient() {

val onPageFinishedCallbacks: MutableList<OnPageFinishedCallback> = mutableListOf()

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
var callbacksExecuted: Boolean = false

override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
Expand Down Expand Up @@ -94,6 +98,7 @@ open class PageWebViewClient : WebViewClient() {
super.onPageFinished(view, url)
if (view == null) return
onPageFinishedCallbacks.map { callback -> callback.onPageFinished(view) }
callbacksExecuted = true
if (promiseToWaitFor == null) {
/** [PageFragment.webView] is invisible by default to avoid flashes while
* the page is loaded, and can be made visible again after it finishes loading */
Expand Down

0 comments on commit 6f5452b

Please # to comment.