From e485fcb5f4bddc8c61709c60a046157c4474fb51 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:36:00 +0100 Subject: [PATCH 1/8] test(reminders): add `withScheduleRemindersFragment` Part of 21753 Assisted-by: Claude Fable 5.1 --- .../reviewreminders/ReviewRemindersScreenshotTest.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index d56aecd42eee..22c26ed93d43 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -157,9 +157,9 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { @Test fun `standalone activity host with system bars and a scrollable list`() { insertReminders(count = 12) - withStandaloneScheduleReminders { activity -> - activity.simulateSystemBars() - val binding = FragmentScheduleRemindersBinding.bind(activity.fragment!!.requireView()) + withScheduleRemindersFragment { fragment -> + fragment.requireActivity().simulateSystemBars() + val binding = FragmentScheduleRemindersBinding.bind(fragment.requireView()) // scrolled to the end: the last reminder must clear the navigation bar band binding.recyclerView.scrollToLastPosition() advanceRobolectricLooper() @@ -259,6 +259,10 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { } } + /** Launches [ScheduleRemindersFragment] in its own activity, exposing the fragment */ + private fun withScheduleRemindersFragment(block: (ScheduleRemindersFragment) -> Unit) = + withStandaloneScheduleReminders { activity -> block(activity.fragment as ScheduleRemindersFragment) } + /** Collapses the settings host's toolbar, as when the list has been scrolled */ private fun FragmentManager.collapseToolbar() { findFragmentById(R.id.settings_container) From 95d580326dfc5d7150597121387d056622787dac Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:36:02 +0100 Subject: [PATCH 2/8] test(reminders): use view binding Part of 21753 Assisted-by: Claude Fable 5.1 --- .../ichi2/anki/reviewreminders/ScheduleRemindersFragment.kt | 4 +++- .../anki/reviewreminders/ReviewRemindersScreenshotTest.kt | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ScheduleRemindersFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ScheduleRemindersFragment.kt index e966ca862aad..33671fb2b325 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ScheduleRemindersFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/reviewreminders/ScheduleRemindersFragment.kt @@ -13,6 +13,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.annotation.IdRes +import androidx.annotation.VisibleForTesting import androidx.core.graphics.Insets import androidx.core.view.MenuProvider import androidx.core.view.ViewCompat @@ -161,7 +162,8 @@ class ScheduleRemindersFragment : ?: FragmentHost.SETTINGS } - private val binding by viewBinding(FragmentScheduleRemindersBinding::bind) + @VisibleForTesting + internal val binding by viewBinding(FragmentScheduleRemindersBinding::bind) private val troubleshootingViewModel: ReminderTroubleshootingViewModel by activityViewModels { reminderTroubleshootingViewModelFactory(requireContext()) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index 22c26ed93d43..3039416c5b03 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -14,7 +14,6 @@ import com.ichi2.anki.StudyOptionsActivity import com.ichi2.anki.common.destinations.StudyOptionsDestination import com.ichi2.anki.common.destinations.launchActivity import com.ichi2.anki.databinding.FragmentReminderTroubleshootingBinding -import com.ichi2.anki.databinding.FragmentScheduleRemindersBinding import com.ichi2.anki.preferences.PreferencesActivity import com.ichi2.anki.preferences.PreferencesFragment import com.ichi2.anki.reviewreminders.ScheduleRemindersFragment.FragmentHost @@ -159,7 +158,7 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { insertReminders(count = 12) withScheduleRemindersFragment { fragment -> fragment.requireActivity().simulateSystemBars() - val binding = FragmentScheduleRemindersBinding.bind(fragment.requireView()) + val binding = fragment.binding // scrolled to the end: the last reminder must clear the navigation bar band binding.recyclerView.scrollToLastPosition() advanceRobolectricLooper() From 85e8cf33fe3ce482b644776b7184fb6db1b6f10a Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:00:49 +0100 Subject: [PATCH 3/8] test(reminders): clear reminders between tests Part of 21753 Assisted-by: GPT-6 --- .../reviewreminders/ReviewRemindersScreenshotTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index 3039416c5b03..a5f808c4b7d0 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -4,6 +4,7 @@ package com.ichi2.anki.reviewreminders import androidx.annotation.IdRes +import androidx.core.content.edit import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.test.core.app.ActivityScenario @@ -24,6 +25,8 @@ import com.ichi2.testutils.scrollToLastPosition import com.ichi2.testutils.simulateSystemBars import com.ichi2.utils.dp import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before import org.junit.Test import org.robolectric.RuntimeEnvironment @@ -31,6 +34,13 @@ import org.robolectric.RuntimeEnvironment * Covers all [FragmentHost] configurations of the fragment. */ class ReviewRemindersScreenshotTest : ScreenshotTest() { + @Before + @After + fun clearReminders() { + // The database retains its own SharedPreferences instance across Robolectric test cases. + ReviewRemindersDatabase.remindersSharedPrefs.edit { clear() } + } + @Test fun `settings host`() { captureSettingsHost("settingsHost") From 2cd257b5cc1a8474fcc0b2b7dc45be6439657ce2 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:48:06 +0100 Subject: [PATCH 4/8] test(reminders): screenshot enabled and disabled reminders Cover global and deck-specific reminders in the populated schedule view. Part of 21753 Assisted-by: GPT-6 --- .../ReviewRemindersScreenshotTest.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index a5f808c4b7d0..d08e9e2c052e 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -156,6 +156,19 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { ) } + @Test + fun `enabled and disabled global and deck reminders`() { + val deckScope = ReviewReminderScope.DeckSpecific(addDeck("Japanese::Vocabulary")) + insertReminder(ReviewReminderTime(8, 0)) + insertReminder(ReviewReminderTime(9, 30), enabled = false) + insertReminder(ReviewReminderTime(18, 15), scope = deckScope) + insertReminder(ReviewReminderTime(21, 45), scope = deckScope, enabled = false) + + withStandaloneScheduleReminders { + captureScreen("globalAndDeckReminders_enabledAndDisabled") + } + } + @Test fun `standalone activity host with system bars`() = withStandaloneScheduleReminders { activity -> @@ -203,6 +216,16 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { } } + private fun insertReminder( + time: ReviewReminderTime, + scope: ReviewReminderScope = ReviewReminderScope.Global, + enabled: Boolean = true, + ) = runBlocking { + ReviewRemindersDatabase.insertReminder( + ReviewReminder.createReviewReminder(time = time, scope = scope, enabled = enabled), + ) + } + /** Inserts [count] reminders so the list has content to render behind the simulated bars */ private fun insertReminders(count: Int) { runBlocking { From fe7f946c0631dc32f1928bc1e4eff544c6846595 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:49:48 +0100 Subject: [PATCH 5/8] test(reminders): screenshot reminders for a deleted deck Cover enabled and disabled reminders after their associated deck is removed. Part of 21753 Assisted-by: GPT-6 --- .../ReviewRemindersScreenshotTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index d08e9e2c052e..669287cf6f35 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -169,6 +169,20 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { } } + @Test + fun `reminders for a deleted deck`() { + val deckId = addDeck("Deleted deck") + val deckScope = ReviewReminderScope.DeckSpecific(deckId) + insertReminder(ReviewReminderTime(8, 0)) + insertReminder(ReviewReminderTime(9, 30), scope = deckScope) + insertReminder(ReviewReminderTime(18, 15), scope = deckScope, enabled = false) + col.decks.remove(listOf(deckId)) + + withStandaloneScheduleReminders { + captureScreen("deletedDeckReminders") + } + } + @Test fun `standalone activity host with system bars`() = withStandaloneScheduleReminders { activity -> From cc20a4f3bfa4a6ccc4cf84950607f620ab652e81 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:53:34 +0100 Subject: [PATCH 6/8] test(reminders): screenshot notification permission bottom sheet Capture the permission request shown after saving a first reminder. Part of 21753 Assisted-by: GPT-6 --- .../ReviewRemindersScreenshotTest.kt | 40 +++++++++++++++++++ .../ichi2/testutils/DialogFragmentUtils.kt | 12 ++++++ 2 files changed, 52 insertions(+) create mode 100644 AnkiDroid/src/test/java/com/ichi2/testutils/DialogFragmentUtils.kt diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index 669287cf6f35..710ecb125228 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -3,8 +3,10 @@ package com.ichi2.anki.reviewreminders +import android.app.NotificationManager import androidx.annotation.IdRes import androidx.core.content.edit +import androidx.core.content.getSystemService import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.test.core.app.ActivityScenario @@ -18,9 +20,13 @@ import com.ichi2.anki.databinding.FragmentReminderTroubleshootingBinding import com.ichi2.anki.preferences.PreferencesActivity import com.ichi2.anki.preferences.PreferencesFragment import com.ichi2.anki.reviewreminders.ScheduleRemindersFragment.FragmentHost +import com.ichi2.anki.settings.Prefs +import com.ichi2.anki.ui.windows.permissions.PermissionsBottomSheet +import com.ichi2.anki.ui.windows.permissions.PermissionsFragment import com.ichi2.anki.utils.ConfigAwareSingleFragmentActivity import com.ichi2.anki.withDeckPicker import com.ichi2.testutils.BackupManagerTestUtilities +import com.ichi2.testutils.positiveButton import com.ichi2.testutils.scrollToLastPosition import com.ichi2.testutils.simulateSystemBars import com.ichi2.utils.dp @@ -29,6 +35,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf /** * Covers all [FragmentHost] configurations of the fragment. @@ -183,6 +190,25 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { } } + @Test + fun `notification permission bottom sheet after adding a reminder`() { + shadowOf(targetContext.getSystemService()!!).setNotificationsEnabled(false) + Prefs.reminderNotifsRequestShown = false + Prefs.notificationsPermissionRequested = false + + withScheduleRemindersFragment { fragment -> + fragment.binding.floatingActionButtonAdd.performClick() + advanceRobolectricLooper() + + fragment.addEditReminderDialog.positiveButton.performClick() + advanceRobolectricLooperUntil { + fragment.permissionsBottomSheet?.permissionsFragment?.view != null && + fragment.reminderCount == 1 + } + captureScreen("notificationPermissionBottomSheet") + } + } + @Test fun `standalone activity host with system bars`() = withStandaloneScheduleReminders { activity -> @@ -317,4 +343,18 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { ?.setExpanded(false, false) advanceRobolectricLooper() } + + private val ScheduleRemindersFragment.addEditReminderDialog: AddEditReminderDialog + get() = childFragmentManager.fragments.filterIsInstance().single() + + private val ScheduleRemindersFragment.permissionsBottomSheet: PermissionsBottomSheet? + get() = childFragmentManager.fragments.filterIsInstance().singleOrNull() + + /** The number of reminders in the list */ + private val ScheduleRemindersFragment.reminderCount: Int + get() = binding.recyclerView.adapter!!.itemCount + + /** The content the sheet hosts, once it has been committed */ + private val PermissionsBottomSheet.permissionsFragment: PermissionsFragment? + get() = childFragmentManager.fragments.filterIsInstance().singleOrNull() } diff --git a/AnkiDroid/src/test/java/com/ichi2/testutils/DialogFragmentUtils.kt b/AnkiDroid/src/test/java/com/ichi2/testutils/DialogFragmentUtils.kt new file mode 100644 index 000000000000..90d0bada38fd --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/testutils/DialogFragmentUtils.kt @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package com.ichi2.testutils + +import android.widget.Button +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.ichi2.utils.positiveButton + +/** The positive button of the [AlertDialog] shown by a [DialogFragment] */ +val DialogFragment.positiveButton: Button + get() = (requireDialog() as AlertDialog).positiveButton From ec287757df86bafecf1870cf474a7b3240fc7b91 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:54:28 +0100 Subject: [PATCH 7/8] test(reminders): screenshot legacy notification permission sheet Capture the legacy permission content directly on the suite's SDK to avoid loading the native backend in another classloader. Part of 21753 Assisted-by: GPT-6 --- .../reviewreminders/ReviewRemindersScreenshotTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index 710ecb125228..78e77778dd90 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -11,6 +11,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.commit import androidx.test.core.app.ActivityScenario import com.google.android.material.appbar.AppBarLayout +import com.ichi2.anki.OptionalPermissionSet import com.ichi2.anki.R import com.ichi2.anki.ScreenshotTest import com.ichi2.anki.StudyOptionsActivity @@ -209,6 +210,17 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { } } + @Test + fun `legacy notification permission bottom sheet`() { + shadowOf(targetContext.getSystemService()!!).setNotificationsEnabled(false) + withScheduleRemindersFragment { fragment -> + // Capture the legacy content on the suite's SDK; mixing SDKs cannot share the native backend. + PermissionsBottomSheet.launch(fragment.childFragmentManager, OptionalPermissionSet.LEGACY_NOTIFICATIONS) + advanceRobolectricLooper() + captureScreen("legacyNotificationPermissionBottomSheet") + } + } + @Test fun `standalone activity host with system bars`() = withStandaloneScheduleReminders { activity -> From 9e34bcb6e5f22c6a0ea75c491096ea561fabc5e6 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:23:04 +0100 Subject: [PATCH 8/8] test(reminders): better scrolled screenshot Overflow the list, so the toolbar collapses and we scroll to the bottom. Part of 21753 Assisted-by: Claude Fable 5.1 --- .../ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt index 78e77778dd90..7a63eae9b221 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/reviewreminders/ReviewRemindersScreenshotTest.kt @@ -51,6 +51,8 @@ class ReviewRemindersScreenshotTest : ScreenshotTest() { @Test fun `settings host`() { + // overflow the list: the toolbar only collapses once the list can scroll + insertReminders(count = 12) captureSettingsHost("settingsHost") }