diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt index 0d44a9a41..9fd525d38 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepository.kt @@ -38,6 +38,11 @@ interface DebugMenuRepository { */ fun resetOnboardingStatusForCurrentUser() + /** + * Resets the value for the show coach mark statuses so their default values will be used. + */ + fun resetCoachMarkTourStatuses() + /** * Manipulates the state to force showing the onboarding carousel. * diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt index ea07d821f..5575957d5 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryImpl.kt @@ -56,6 +56,11 @@ class DebugMenuRepositoryImpl( ) } + override fun resetCoachMarkTourStatuses() { + settingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = null) + settingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = null) + } + override fun modifyStateToShowOnboardingCarousel( userStateUpdateTrigger: () -> Unit, ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt index 199257ac1..e69a4a4a9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkContainer.kt @@ -60,7 +60,6 @@ fun <T : Enum<T>> CoachMarkContainer( modifier: Modifier = Modifier, content: @Composable CoachMarkScope<T>.() -> Unit, ) { - val scope = rememberCoroutineScope() Box( modifier = Modifier .fillMaxSize() @@ -101,9 +100,8 @@ fun <T : Enum<T>> CoachMarkContainer( .pointerInput(Unit) { detectTapGestures( onTap = { - scope.launch { - state.showToolTipForCurrentCoachMark() - } + // NO-OP, this consumes any touch events + // while the scrim is showing. }, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt index f28a3e3a0..c7d3b9077 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkScopeInstance.kt @@ -9,8 +9,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults -import androidx.compose.material3.TooltipState -import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -32,6 +30,8 @@ import com.x8bit.bitwarden.ui.platform.base.util.toListItemCardStyle import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape import com.x8bit.bitwarden.ui.platform.components.model.CardStyle import com.x8bit.bitwarden.ui.platform.components.tooltip.BitwardenToolTip +import com.x8bit.bitwarden.ui.platform.components.tooltip.BitwardenToolTipState +import com.x8bit.bitwarden.ui.platform.components.tooltip.rememberBitwardenToolTipState import okhttp3.internal.toImmutableList import org.jetbrains.annotations.VisibleForTesting @@ -55,7 +55,7 @@ class CoachMarkScopeInstance<T : Enum<T>>( rightAction: @Composable() (RowScope.() -> Unit)?, anchorContent: @Composable () -> Unit, ) { - val toolTipState = rememberTooltipState( + val toolTipState = rememberBitwardenToolTipState( initialIsVisible = false, isPersistent = true, ) @@ -192,7 +192,7 @@ class CoachMarkScopeInstance<T : Enum<T>>( leftAction: @Composable() (RowScope.() -> Unit)?, rightAction: @Composable() (RowScope.() -> Unit)?, modifier: Modifier = Modifier, - toolTipState: TooltipState = rememberTooltipState( + toolTipState: BitwardenToolTipState = rememberBitwardenToolTipState( initialIsVisible = false, isPersistent = true, ), diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt index c833f8cbc..2fa1bf09e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/CoachMarkState.kt @@ -1,8 +1,7 @@ package com.x8bit.bitwarden.ui.platform.components.coachmark -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TooltipState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.Saver @@ -11,6 +10,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.geometry.Rect import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightShape import com.x8bit.bitwarden.ui.platform.components.coachmark.model.CoachMarkHighlightState +import com.x8bit.bitwarden.ui.platform.components.tooltip.BitwardenToolTipState import java.util.concurrent.ConcurrentHashMap import kotlin.math.max import kotlin.math.min @@ -27,7 +27,7 @@ import kotlin.math.min * none should be highlighted at start. * @param isCoachMarkVisible is any coach mark currently visible. */ -@OptIn(ExperimentalMaterial3Api::class) +@Stable open class CoachMarkState<T : Enum<T>>( val orderedList: List<T>, initialCoachMarkHighlight: T? = null, @@ -58,7 +58,7 @@ open class CoachMarkState<T : Enum<T>>( fun updateHighlight( key: T, bounds: Rect?, - toolTipState: TooltipState, + toolTipState: BitwardenToolTipState, shape: CoachMarkHighlightShape = CoachMarkHighlightShape.SQUARE, ) { highlights[key] = CoachMarkHighlightState( @@ -193,9 +193,9 @@ open class CoachMarkState<T : Enum<T>>( /** * Cleans up the tooltip state by dismissing it if visible and calling onDispose. */ - private fun TooltipState.cleanUp() { + private fun BitwardenToolTipState.cleanUp() { if (isVisible) { - dismiss() + dismissBitwardenToolTip() } onDispose() } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/LazyListCoachMarkState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/LazyListCoachMarkState.kt index 1973efe6b..7b89dee2b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/LazyListCoachMarkState.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/LazyListCoachMarkState.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable @@ -13,6 +14,7 @@ import androidx.compose.runtime.saveable.rememberSaveable * A [CoachMarkState] that depends on a [LazyListState] to automatically scroll to the current * Coach Mark if not on currently on the screen. */ +@Stable class LazyListCoachMarkState<T : Enum<T>>( private val lazyListState: LazyListState, orderedList: List<T>, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/model/CoachMarkHighlightState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/model/CoachMarkHighlightState.kt index da0ec745f..4ea4fa882 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/model/CoachMarkHighlightState.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/coachmark/model/CoachMarkHighlightState.kt @@ -1,8 +1,7 @@ package com.x8bit.bitwarden.ui.platform.components.coachmark.model -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TooltipState import androidx.compose.ui.geometry.Rect +import com.x8bit.bitwarden.ui.platform.components.tooltip.BitwardenToolTipState /** * Represents a highlight within a coach mark sequence. @@ -13,10 +12,9 @@ import androidx.compose.ui.geometry.Rect * @property toolTipState The state of the tooltip associated with this highlight. * @property shape The shape of the highlight (e.g., square, oval). */ -@OptIn(ExperimentalMaterial3Api::class) data class CoachMarkHighlightState<T : Enum<T>>( val key: T, val highlightBounds: Rect?, - val toolTipState: TooltipState, + val toolTipState: BitwardenToolTipState, val shape: CoachMarkHighlightShape, ) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipState.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipState.kt new file mode 100644 index 000000000..3d877ede8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipState.kt @@ -0,0 +1,17 @@ +package com.x8bit.bitwarden.ui.platform.components.tooltip + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipState + +/** + * A custom [TooltipState] to be used for the tool tips which should not be + * dismissed automatically by clicking outside of the pop-up area. + */ +@OptIn(ExperimentalMaterial3Api::class) +interface BitwardenToolTipState : TooltipState { + /** + * Call to dismiss the tool tip from the screen, should be used in + * place of [TooltipState.dismiss] + */ + fun dismissBitwardenToolTip() +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipStateImpl.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipStateImpl.kt new file mode 100644 index 000000000..a2317e572 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/tooltip/BitwardenToolTipStateImpl.kt @@ -0,0 +1,112 @@ +package com.x8bit.bitwarden.ui.platform.components.tooltip + +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.MutatorMutex +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withTimeout + +/** + * Default implementation of [BitwardenToolTipState] + */ +class BitwardenToolTipStateImpl( + initialIsVisible: Boolean, + override val isPersistent: Boolean, + private val mutatorMutex: MutatorMutex, +) : BitwardenToolTipState { + + override fun dismissBitwardenToolTip() { + transition.targetState = false + } + + override val transition: MutableTransitionState<Boolean> = + MutableTransitionState(initialIsVisible) + + override val isVisible: Boolean + get() = transition.currentState || transition.targetState + + /** continuation used to clean up */ + private var job: (CancellableContinuation<Unit>)? = null + + /** + * Show the tooltip associated with the current [TooltipState]. When this method is called, all + * of the other tooltips associated with [mutatorMutex] will be dismissed. + * + * @param mutatePriority [MutatePriority] to be used with [mutatorMutex]. + */ + override suspend fun show(mutatePriority: MutatePriority) { + val cancellableShow: suspend () -> Unit = { + suspendCancellableCoroutine { continuation -> + transition.targetState = true + job = continuation + } + } + + // Show associated tooltip for [TooltipDuration] amount of time + // or until tooltip is explicitly dismissed depending on [isPersistent]. + mutatorMutex.mutate(mutatePriority) { + try { + if (isPersistent) { + cancellableShow() + } else { + withTimeout(BITWARDEN_TOOL_TIP_TIMEOUT) { cancellableShow() } + } + } finally { + if (mutatePriority != MutatePriority.PreventUserInput) { + // timeout or cancellation has occurred and we close out the current tooltip. + dismissBitwardenToolTip() + } + } + } + } + + /** + * We are overriding this specifically to make it so it is a no-op this prevents the + * tooltip from being dismissed if the user taps anywhere out of it. + */ + override fun dismiss() { + /**No-Op**/ + } + + /** Cleans up [mutatorMutex] when the tooltip associated with this state leaves Composition. */ + override fun onDispose() { + job?.cancel() + } +} + +/** + * Provides a [BitwardenToolTipState] in a composable scope remembered across compositions. + * + * @param mutatorMutex if providing your own, ensure that any tool tips that should be + * shown/hidden in the context of the one you are using this state for, make use of the same + * instance of the passed in value. + */ +@Composable +@ExperimentalMaterial3Api +fun rememberBitwardenToolTipState( + initialIsVisible: Boolean = false, + isPersistent: Boolean = false, + mutatorMutex: MutatorMutex = BitwardenToolTipStateDefaults.GlobalMutatorMutex, +): BitwardenToolTipState = + remember(isPersistent, mutatorMutex) { + BitwardenToolTipStateImpl( + initialIsVisible = initialIsVisible, + isPersistent = isPersistent, + mutatorMutex = mutatorMutex, + ) + } + +/** + * Provides a global [MutatorMutex] as a singleton to be used by default for each + * created [BitwardenToolTipState] + */ +private object BitwardenToolTipStateDefaults { + val GlobalMutatorMutex = MutatorMutex() +} + +private const val BITWARDEN_TOOL_TIP_TIMEOUT = 1500L diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt index 8083d84b4..ba669c53b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreen.kt @@ -115,6 +115,19 @@ fun DebugMenuScreen( } }, ) + Spacer(Modifier.height(16.dp)) + BitwardenFilledButton( + label = stringResource(R.string.reset_coach_mark_tour_status), + onClick = remember(viewModel) { + { + viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses) + } + }, + isEnabled = true, + modifier = Modifier + .fillMaxWidth() + .standardHorizontalMargin(), + ) Spacer(modifier = Modifier.height(height = 16.dp)) Spacer(modifier = Modifier.navigationBarsPadding()) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt index e79702f47..97580e23d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModel.kt @@ -48,9 +48,14 @@ class DebugMenuViewModel @Inject constructor( DebugMenuAction.ResetFeatureFlagValues -> handleResetFeatureFlagValues() DebugMenuAction.RestartOnboarding -> handleResetOnboardingStatus() DebugMenuAction.RestartOnboardingCarousel -> handleResetOnboardingCarousel() + DebugMenuAction.ResetCoachMarkTourStatuses -> handleResetCoachMarkTourStatuses() } } + private fun handleResetCoachMarkTourStatuses() { + debugMenuRepository.resetCoachMarkTourStatuses() + } + private fun handleResetOnboardingCarousel() { debugMenuRepository.modifyStateToShowOnboardingCarousel( userStateUpdateTrigger = { @@ -133,6 +138,11 @@ sealed class DebugMenuAction { */ data object RestartOnboardingCarousel : DebugMenuAction() + /** + * User has clicked to reset coach mark values. + */ + data object ResetCoachMarkTourStatuses : DebugMenuAction() + /** * Internal actions not triggered from the UI. */ diff --git a/app/src/main/res/values/strings_non_localized.xml b/app/src/main/res/values/strings_non_localized.xml index d9aa5fd51..c2241ac67 100644 --- a/app/src/main/res/values/strings_non_localized.xml +++ b/app/src/main/res/values/strings_non_localized.xml @@ -25,5 +25,6 @@ <string name="new_device_permanent_dismiss">New device notice permanent dismiss</string>"> <string name="new_device_temporary_dismiss">New device notice temporary dismiss</string>"> <string name="ignore_environment_check">Ignore environment check</string>"> + <string name="reset_coach_mark_tour_status">Reset all coach mark tours</string> <!-- /Debug Menu --> </resources> diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt index 12bc28097..71a76d1d4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/repository/DebugMenuRepositoryTest.kt @@ -203,6 +203,23 @@ class DebugMenuRepositoryTest { } assertTrue(lambdaHasBeenCalled) } + + @Test + fun `resetCoachMarkTourStatuses calls settings disk source setting values back to null`() { + every { + mockSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = any()) + } just runs + every { + mockSettingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = any()) + } just runs + + debugMenuRepository.resetCoachMarkTourStatuses() + + verify(exactly = 1) { + mockSettingsDiskSource.storeShouldShowGeneratorCoachMark(shouldShow = null) + mockSettingsDiskSource.storeShouldShowAddLoginCoachMark(shouldShow = null) + } + } } private const val TEST_STRING_VALUE = "test" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt index 974b91872..1d54c58f6 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuScreenTest.kt @@ -89,7 +89,7 @@ class DebugMenuScreenTest : BaseComposeTest() { .onNodeWithText("Email Verification", ignoreCase = true) .performClick() - verify { + verify(exactly = 1) { viewModel.trySendAction( DebugMenuAction.UpdateFeatureFlag( FlagKey.EmailVerification, @@ -106,7 +106,7 @@ class DebugMenuScreenTest : BaseComposeTest() { .performScrollTo() .performClick() - verify { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) } + verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues) } } @Test @@ -124,7 +124,7 @@ class DebugMenuScreenTest : BaseComposeTest() { .assertIsEnabled() .performClick() - verify { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) } + verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) } } @Test @@ -161,7 +161,7 @@ class DebugMenuScreenTest : BaseComposeTest() { .assertIsEnabled() .performClick() - verify { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) } + verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) } } @Test @@ -182,4 +182,14 @@ class DebugMenuScreenTest : BaseComposeTest() { verify(exactly = 0) { viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) } } + + @Test + fun `reset all coach mark tours should send ResetCoachMarkTourStatuses action`() { + composeTestRule + .onNodeWithText("Reset all coach mark tours") + .performScrollTo() + .performClick() + + verify(exactly = 1) { viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses) } + } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt index 20dc42136..ce0e2481c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/debugmenu/DebugMenuViewModelTest.kt @@ -78,14 +78,16 @@ class DebugMenuViewModelTest : BaseViewModelTest() { viewModel.trySendAction( DebugMenuAction.UpdateFeatureFlag(FlagKey.EmailVerification, false), ) - verify { mockDebugMenuRepository.updateFeatureFlag(FlagKey.EmailVerification, false) } + verify(exactly = 1) { + mockDebugMenuRepository.updateFeatureFlag(FlagKey.EmailVerification, false) + } } @Test fun `handleResetOnboardingStatus should reset the onboarding status`() { val viewModel = createViewModel() viewModel.trySendAction(DebugMenuAction.RestartOnboarding) - verify { mockDebugMenuRepository.resetOnboardingStatusForCurrentUser() } + verify(exactly = 1) { mockDebugMenuRepository.resetOnboardingStatusForCurrentUser() } } @Suppress("MaxLineLength") @@ -93,12 +95,21 @@ class DebugMenuViewModelTest : BaseViewModelTest() { fun `handleResetOnboardingCarousel should reset the onboarding carousel and update user state pending account action`() { val viewModel = createViewModel() viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel) - verify { + verify(exactly = 1) { mockDebugMenuRepository.modifyStateToShowOnboardingCarousel(any()) mockAuthRepository.hasPendingAccountAddition = true } } + @Test + fun `handleResetCoachMarkTourStatuses should call repository to reset values`() { + val viewModel = createViewModel() + viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses) + verify(exactly = 1) { + mockDebugMenuRepository.resetCoachMarkTourStatuses() + } + } + private fun createViewModel(): DebugMenuViewModel = DebugMenuViewModel( featureFlagManager = mockFeatureFlagManager, debugMenuRepository = mockDebugMenuRepository,