@@ -25,14 +25,14 @@ import com.duckduckgo.app.InstantSchedulersRule
2525import com.duckduckgo.app.cta.db.DismissedCtaDao
2626import com.duckduckgo.app.cta.model.CtaId
2727import com.duckduckgo.app.cta.model.DismissedCta
28- import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
2928import com.duckduckgo.app.global.db.AppDatabase
30- import com.duckduckgo.app.global.install.AppInstallStore
31- import com.duckduckgo.app.global.model.Site
3229import com.duckduckgo.app.global.events.db.UserEventEntity
33- import com.duckduckgo.app.global.events.db.UserEventsStore
3430import com.duckduckgo.app.global.events.db.UserEventKey
31+ import com.duckduckgo.app.global.events.db.UserEventsStore
32+ import com.duckduckgo.app.global.install.AppInstallStore
33+ import com.duckduckgo.app.global.model.Site
3534import com.duckduckgo.app.global.useourapp.UseOurAppDetector
35+ import com.duckduckgo.app.global.useourapp.UseOurAppDetector.Companion.USE_OUR_APP_SHORTCUT_URL
3636import com.duckduckgo.app.onboarding.store.AppStage
3737import com.duckduckgo.app.onboarding.store.OnboardingStore
3838import com.duckduckgo.app.onboarding.store.UserStageStore
@@ -43,7 +43,9 @@ import com.duckduckgo.app.privacy.model.PrivacyPractices
4343import com.duckduckgo.app.privacy.model.TestEntity
4444import com.duckduckgo.app.runBlocking
4545import com.duckduckgo.app.settings.db.SettingsDataStore
46+ import com.duckduckgo.app.statistics.Variant
4647import com.duckduckgo.app.statistics.VariantManager
48+ import com.duckduckgo.app.statistics.VariantManager.Companion.DEFAULT_VARIANT
4749import com.duckduckgo.app.statistics.pixels.Pixel
4850import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.*
4951import com.duckduckgo.app.survey.db.SurveyDao
@@ -54,6 +56,11 @@ import com.duckduckgo.app.trackerdetection.model.TrackingEvent
5456import com.duckduckgo.app.widget.ui.WidgetCapabilities
5557import com.nhaarman.mockitokotlin2.*
5658import kotlinx.coroutines.ExperimentalCoroutinesApi
59+ import kotlinx.coroutines.channels.Channel
60+ import kotlinx.coroutines.flow.collect
61+ import kotlinx.coroutines.flow.consumeAsFlow
62+ import kotlinx.coroutines.flow.first
63+ import kotlinx.coroutines.launch
5764import kotlinx.coroutines.test.runBlockingTest
5865import org.junit.After
5966import org.junit.Assert.*
@@ -114,6 +121,8 @@ class CtaViewModelTest {
114121 @Mock
115122 private lateinit var mockUserEventsStore: UserEventsStore
116123
124+ private val dismissedCtaDaoChannel = Channel <List <DismissedCta >>()
125+
117126 private val requiredDaxOnboardingCtas: List <CtaId > = listOf (
118127 CtaId .DAX_INTRO ,
119128 CtaId .DAX_DIALOG_SERP ,
@@ -122,6 +131,15 @@ class CtaViewModelTest {
122131 CtaId .DAX_END
123132 )
124133
134+ private val requiredFireEducationDaxOnboardingCtas: List <CtaId > = listOf (
135+ CtaId .DAX_INTRO ,
136+ CtaId .DAX_DIALOG_SERP ,
137+ CtaId .DAX_DIALOG_TRACKERS_FOUND ,
138+ CtaId .DAX_DIALOG_NETWORK ,
139+ CtaId .DAX_FIRE_BUTTON ,
140+ CtaId .DAX_END
141+ )
142+
125143 private lateinit var testee: CtaViewModel
126144
127145 @Before
@@ -135,6 +153,8 @@ class CtaViewModelTest {
135153
136154 whenever(mockAppInstallStore.installTimestamp).thenReturn(System .currentTimeMillis() - TimeUnit .DAYS .toMillis(1 ))
137155 whenever(mockUserWhitelistDao.contains(any())).thenReturn(false )
156+ whenever(mockDismissedCtaDao.dismissedCtas()).thenReturn(dismissedCtaDaoChannel.consumeAsFlow())
157+ givenControlGroup()
138158
139159 testee = CtaViewModel (
140160 mockAppInstallStore,
@@ -155,6 +175,7 @@ class CtaViewModelTest {
155175
156176 @After
157177 fun after () {
178+ dismissedCtaDaoChannel.close()
158179 db.close()
159180 }
160181
@@ -231,6 +252,33 @@ class CtaViewModelTest {
231252 verify(mockUserStageStore).stageCompleted(AppStage .DAX_ONBOARDING )
232253 }
233254
255+ @Test
256+ fun whenFireEducationEnabledCtaDismissedAndUserHasPendingOnboardingCtasThenStageNotCompleted () = coroutineRule.runBlocking {
257+ givenFireButtonEducationActive()
258+ givenOnboardingActive()
259+ givenShownDaxOnboardingCtas(emptyList())
260+ testee.onUserDismissedCta(DaxBubbleCta .DaxEndCta (mockOnboardingStore, mockAppInstallStore))
261+ verify(mockUserStageStore, times(0 )).stageCompleted(any())
262+ }
263+
264+ @Test
265+ fun whenFireEducationEnabledAndCtaDismissedAndAllDaxOnboardingCtasShownThenStageNotCompleted () = coroutineRule.runBlocking {
266+ givenFireButtonEducationActive()
267+ givenOnboardingActive()
268+ givenShownDaxOnboardingCtas(requiredDaxOnboardingCtas)
269+ testee.onUserDismissedCta(DaxDialogCta .DaxSerpCta (mockOnboardingStore, mockAppInstallStore))
270+ verify(mockUserStageStore, times(0 )).stageCompleted(any())
271+ }
272+
273+ @Test
274+ fun whenFireEducationEnabledAndCtaDismissedAndAllFireEducationDaxOnboardingCtasShownThenStageCompleted () = coroutineRule.runBlocking {
275+ givenFireButtonEducationActive()
276+ givenOnboardingActive()
277+ givenShownDaxOnboardingCtas(requiredFireEducationDaxOnboardingCtas)
278+ testee.onUserDismissedCta(DaxDialogCta .DaxSerpCta (mockOnboardingStore, mockAppInstallStore))
279+ verify(mockUserStageStore).stageCompleted(AppStage .DAX_ONBOARDING )
280+ }
281+
234282 @Test
235283 fun whenHideTipsForeverThenPixelIsFired () = coroutineRule.runBlocking {
236284 testee.hideTipsForever(HomePanelCta .AddWidgetAuto )
@@ -513,6 +561,125 @@ class CtaViewModelTest {
513561 verify(mockUserStageStore).stageCompleted(AppStage .USE_OUR_APP_ONBOARDING )
514562 }
515563
564+ @Test
565+ fun whenUserHidAllTipsThenFireButtonAnimationShouldNotShow () = coroutineRule.runBlocking {
566+ givenFireButtonEducationActive()
567+ whenever(mockSettingsDataStore.hideTips).thenReturn(true )
568+ launch {
569+ dismissedCtaDaoChannel.send(emptyList())
570+ }
571+
572+ assertFalse(testee.showFireButtonPulseAnimation.first())
573+ }
574+
575+ @Test
576+ fun whenUserHasAlreadySeenFireButtonCtaThenFireButtonAnimationShouldNotShow () = coroutineRule.runBlocking {
577+ givenFireButtonEducationActive()
578+ whenever(mockDismissedCtaDao.exists(CtaId .DAX_FIRE_BUTTON )).thenReturn(true )
579+ launch {
580+ dismissedCtaDaoChannel.send(emptyList())
581+ }
582+
583+ assertFalse(testee.showFireButtonPulseAnimation.first())
584+ }
585+
586+ @Test
587+ fun whenTipsAndFireOnboardingActiveAndUserSeesAnyTriggerFirePulseAnimationCtaThenFireButtonAnimationShouldShow () = coroutineRule.runBlocking {
588+ givenFireButtonEducationActive()
589+ givenOnboardingActive()
590+ val willTriggerFirePulseAnimationCtas = listOf (CtaId .DAX_DIALOG_TRACKERS_FOUND , CtaId .DAX_DIALOG_NETWORK , CtaId .DAX_DIALOG_OTHER )
591+
592+ val launch = launch {
593+ testee.showFireButtonPulseAnimation.collect {
594+ assertTrue(it)
595+ }
596+ }
597+ willTriggerFirePulseAnimationCtas.forEach {
598+ dismissedCtaDaoChannel.send(listOf (DismissedCta (it)))
599+ }
600+
601+ launch.cancel()
602+ }
603+
604+ @Test
605+ fun whenTipsAndFireOnboardingActiveAndUserSeesAnyNonTriggerFirePulseAnimationCtaThenFireButtonAnimationShouldNotShow () = coroutineRule.runBlocking {
606+ givenFireButtonEducationActive()
607+ givenOnboardingActive()
608+ val willTriggerFirePulseAnimationCtas = listOf (CtaId .DAX_DIALOG_TRACKERS_FOUND , CtaId .DAX_DIALOG_NETWORK , CtaId .DAX_DIALOG_OTHER )
609+ val willNotTriggerFirePulseAnimationCtas = CtaId .values().toList() - willTriggerFirePulseAnimationCtas
610+
611+ val launch = launch {
612+ testee.showFireButtonPulseAnimation.collect {
613+ assertFalse(it)
614+ }
615+ }
616+ willNotTriggerFirePulseAnimationCtas.forEach {
617+ dismissedCtaDaoChannel.send(listOf (DismissedCta (it)))
618+ }
619+
620+ launch.cancel()
621+ }
622+
623+ @Test
624+ fun whenFireEducationDisabledAndUserSeesAnyCtaThenFireButtonAnimationShouldNotShow () = coroutineRule.runBlocking {
625+ givenControlGroup()
626+ givenOnboardingActive()
627+ val allCtas = CtaId .values().toList()
628+
629+ val launch = launch {
630+ testee.showFireButtonPulseAnimation.collect {
631+ assertFalse(it)
632+ }
633+ }
634+ allCtas.forEach {
635+ dismissedCtaDaoChannel.send(listOf (DismissedCta (it)))
636+ }
637+
638+ launch.cancel()
639+ }
640+
641+ @Test
642+ fun whenFirstTimeUserClicksOnFireButtonThenFireDialogCtaReturned () = coroutineRule.runBlocking {
643+ givenFireButtonEducationActive()
644+ givenOnboardingActive()
645+
646+ val fireDialogCta = testee.getFireDialogCta()
647+
648+ assertTrue(fireDialogCta is DaxFireDialogCta .TryClearDataCta )
649+ }
650+
651+ @Test
652+ fun whenFirstTimeUserClicksOnFireButtonButUserHidAllTipsThenFireDialogCtaIsNull () = coroutineRule.runBlocking {
653+ givenFireButtonEducationActive()
654+ givenOnboardingActive()
655+ whenever(mockSettingsDataStore.hideTips).thenReturn(true )
656+
657+ val fireDialogCta = testee.getFireDialogCta()
658+
659+ assertNull(fireDialogCta)
660+ }
661+
662+ @Test
663+ fun whenFireCtaDismissedThenFireDialogCtaIsNull () = coroutineRule.runBlocking {
664+ givenFireButtonEducationActive()
665+ givenOnboardingActive()
666+ whenever(mockDismissedCtaDao.exists(CtaId .DAX_FIRE_BUTTON )).thenReturn(true )
667+
668+ val fireDialogCta = testee.getFireDialogCta()
669+
670+ assertNull(fireDialogCta)
671+ }
672+
673+ @Test
674+ fun whenFireEducationDisabledThenFireDialogCtaIsNull () = coroutineRule.runBlocking {
675+ givenControlGroup()
676+ givenOnboardingActive()
677+
678+ val fireDialogCta = testee.getFireDialogCta()
679+
680+ assertNull(fireDialogCta)
681+ }
682+
516683 private suspend fun givenDaxOnboardingActive () {
517684 whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage .DAX_ONBOARDING )
518685 }
@@ -539,6 +706,21 @@ class CtaViewModelTest {
539706 whenever(mockUserStageStore.getUserAppStage()).thenReturn(AppStage .USE_OUR_APP_ONBOARDING )
540707 }
541708
709+ private fun givenFireButtonEducationActive () {
710+ whenever(mockVariantManager.getVariant()).thenReturn(
711+ Variant (
712+ " test" ,
713+ features = listOf (
714+ VariantManager .VariantFeature .FireButtonEducation
715+ ),
716+ filterBy = { true })
717+ )
718+ }
719+
720+ private fun givenControlGroup () {
721+ whenever(mockVariantManager.getVariant()).thenReturn(DEFAULT_VARIANT )
722+ }
723+
542724 private fun site (
543725 url : String = "http : // www.test.com",
544726 uri : Uri ? = Uri .parse(url),
0 commit comments