Skip to content

Commit 25cfa62

Browse files
authored
Add alternative onboarding for interstitial search users (#738)
* Add system search alternative onboarding * Make title bar color primary color so it is continuous with omnibar
1 parent a6bdd69 commit 25cfa62

File tree

40 files changed

+634
-98
lines changed

40 files changed

+634
-98
lines changed

app/src/androidTest/java/com/duckduckgo/app/statistics/ExperimentationVariantManagerTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.duckduckgo.app.statistics
2020

21+
import com.duckduckgo.app.statistics.VariantManager.Companion.RESERVED_EU_AUCTION_VARIANT
2122
import com.duckduckgo.app.statistics.store.StatisticsDataStore
2223
import com.nhaarman.mockitokotlin2.*
2324
import org.junit.Assert.*
@@ -166,12 +167,19 @@ class ExperimentationVariantManagerTest {
166167
}
167168

168169
@Test
169-
fun whenReferrerVariantReturnedThenNoFeaturesEnabled() {
170+
fun whenUnknownReferrerVariantReturnedThenNoFeaturesEnabled() {
170171
mockUpdateScenario("xx")
171172
val variant = testee.getVariant(activeVariants)
172173
assertTrue(variant.features.isEmpty())
173174
}
174175

176+
@Test
177+
fun whenEuAuctionReferrerVariantReturnedThenSuppressWidgetFeaturesEnabled() {
178+
mockUpdateScenario(RESERVED_EU_AUCTION_VARIANT)
179+
val variant = testee.getVariant(activeVariants)
180+
assertTrue(variant.hasFeature(VariantManager.VariantFeature.SuppressHomeTabWidgetCta))
181+
}
182+
175183
private fun mockUpdateScenario(key: String) {
176184
testee.updateAppReferrerVariant(key)
177185
whenever(mockStore.referrerVariant).thenReturn(key)

app/src/androidTest/java/com/duckduckgo/app/statistics/VariantManagerTest.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import org.junit.Test
2525
class VariantManagerTest {
2626

2727
private val variants = VariantManager.ACTIVE_VARIANTS +
28-
variantWithKey(RESERVED_EU_AUCTION_VARIANT) +
28+
VariantManager.REFERRER_VARIANTS +
2929
DEFAULT_VARIANT
3030

3131
// SERP Experiment(s)
@@ -112,9 +112,4 @@ class VariantManagerTest {
112112
fail("Doubles are not equal. Expected $expected but was $actual")
113113
}
114114
}
115-
116-
@Suppress("SameParameterValue")
117-
private fun variantWithKey(key: String): Variant {
118-
return DEFAULT_VARIANT.copy(key = key)
119-
}
120115
}

app/src/androidTest/java/com/duckduckgo/app/systemsearch/SystemSearchViewModelTest.kt

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ import com.duckduckgo.app.InstantSchedulersRule
2424
import com.duckduckgo.app.autocomplete.api.AutoComplete
2525
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteResult
2626
import com.duckduckgo.app.autocomplete.api.AutoComplete.AutoCompleteSuggestion.AutoCompleteSearchSuggestion
27+
import com.duckduckgo.app.onboarding.store.OnboardingStore
28+
import com.duckduckgo.app.statistics.pixels.Pixel
29+
import com.duckduckgo.app.statistics.pixels.Pixel.PixelName.*
2730
import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command
2831
import com.duckduckgo.app.systemsearch.SystemSearchViewModel.Command.LaunchDuckDuckGo
2932
import com.nhaarman.mockitokotlin2.argumentCaptor
@@ -51,8 +54,10 @@ class SystemSearchViewModelTest {
5154
@get:Rule
5255
var coroutineRule = CoroutineTestRule()
5356

57+
private val mockOnboardingStore: OnboardingStore = mock()
5458
private val mockDeviceAppLookup: DeviceAppLookup = mock()
5559
private val mockAutoComplete: AutoComplete = mock()
60+
private val mockPixel: Pixel = mock()
5661

5762
private val commandObserver: Observer<Command> = mock()
5863
private val commandCaptor = argumentCaptor<Command>()
@@ -65,7 +70,7 @@ class SystemSearchViewModelTest {
6570
whenever(mockAutoComplete.autoComplete(BLANK_QUERY)).thenReturn(Observable.just(autocompleteBlankResult))
6671
whenever(mockDeviceAppLookup.query(QUERY)).thenReturn(appQueryResult)
6772
whenever(mockDeviceAppLookup.query(BLANK_QUERY)).thenReturn(appBlankResult)
68-
testee = SystemSearchViewModel(mockAutoComplete, mockDeviceAppLookup, coroutineRule.testDispatcherProvider)
73+
testee = SystemSearchViewModel(mockOnboardingStore, mockAutoComplete, mockDeviceAppLookup, mockPixel, coroutineRule.testDispatcherProvider)
6974
testee.command.observeForever(commandObserver)
7075
}
7176

@@ -74,11 +79,70 @@ class SystemSearchViewModelTest {
7479
testee.command.removeObserver(commandObserver)
7580
}
7681

82+
@Test
83+
fun whenOnboardingShouldNotShowThenViewIsNotVisibleAndUnexpanded() = runBlockingTest {
84+
whenever(mockOnboardingStore.shouldShow).thenReturn(false)
85+
testee.resetViewState()
86+
87+
val viewState = testee.onboardingViewState.value
88+
assertFalse(viewState!!.visible)
89+
assertFalse(viewState!!.expanded)
90+
}
91+
92+
@Test
93+
fun whenOnboardingShouldShowThenViewIsVisibleAndUnexpanded() = runBlockingTest {
94+
whenever(mockOnboardingStore.shouldShow).thenReturn(true)
95+
testee.resetViewState()
96+
97+
val viewState = testee.onboardingViewState.value
98+
assertTrue(viewState!!.visible)
99+
assertFalse(viewState!!.expanded)
100+
}
101+
102+
@Test
103+
fun whenOnboardingShownThenPixelSent() = runBlockingTest {
104+
whenever(mockOnboardingStore.shouldShow).thenReturn(true)
105+
testee.resetViewState()
106+
verify(mockPixel).fire(INTERSTITIAL_ONBOARDING_SHOWN)
107+
}
108+
109+
@Test
110+
fun whenOnboardingIsUnexpandedAndUserPressesToggleThenItIsExpandedAndPixelSent() = runBlockingTest {
111+
whenOnboardingShowing()
112+
testee.userTappedOnboardingToggle()
113+
114+
val viewState = testee.onboardingViewState.value
115+
assertTrue(viewState!!.expanded)
116+
verify(mockPixel).fire(INTERSTITIAL_ONBOARDING_MORE_PRESSED)
117+
}
118+
119+
@Test
120+
fun whenOnboardingIsExpandedAndUserPressesToggleThenItIsUnexpandedAndPixelSent() = runBlockingTest {
121+
whenOnboardingShowing()
122+
testee.userTappedOnboardingToggle() // first press to expand
123+
testee.userTappedOnboardingToggle() // second press to minimize
124+
125+
val viewState = testee.onboardingViewState.value
126+
assertFalse(viewState!!.expanded)
127+
verify(mockPixel).fire(INTERSTITIAL_ONBOARDING_LESS_PRESSED)
128+
}
129+
130+
@Test
131+
fun whenOnboardingIsDismissedThenViewHiddenPixelSentAndOnboardingStoreNotified() = runBlockingTest {
132+
whenOnboardingShowing()
133+
testee.userDismissedOnboarding()
134+
135+
val viewState = testee.onboardingViewState.value
136+
assertFalse(viewState!!.visible)
137+
verify(mockPixel).fire(INTERSTITIAL_ONBOARDING_DISMISSED)
138+
verify(mockOnboardingStore).onboardingShown()
139+
}
140+
77141
@Test
78142
fun whenUserUpdatesQueryThenViewStateUpdated() = ruleRunBlockingTest {
79143
testee.userUpdatedQuery(QUERY)
80144

81-
val newViewState = testee.viewState.value
145+
val newViewState = testee.resultsViewState.value
82146
assertNotNull(newViewState)
83147
assertEquals(QUERY, newViewState?.queryText)
84148
assertEquals(appQueryResult, newViewState?.appResults)
@@ -90,7 +154,7 @@ class SystemSearchViewModelTest {
90154
testee.userUpdatedQuery(QUERY)
91155
testee.userUpdatedQuery("$QUERY ")
92156

93-
val newViewState = testee.viewState.value
157+
val newViewState = testee.resultsViewState.value
94158
assertNotNull(newViewState)
95159
assertEquals("$QUERY ", newViewState?.queryText)
96160
assertEquals(appQueryResult, newViewState?.appResults)
@@ -102,7 +166,7 @@ class SystemSearchViewModelTest {
102166
testee.userUpdatedQuery(QUERY)
103167
testee.userClearedQuery()
104168

105-
val newViewState = testee.viewState.value
169+
val newViewState = testee.resultsViewState.value
106170
assertNotNull(newViewState)
107171
assertTrue(newViewState!!.queryText.isEmpty())
108172
assertTrue(newViewState.appResults.isEmpty())
@@ -114,39 +178,43 @@ class SystemSearchViewModelTest {
114178
testee.userUpdatedQuery(QUERY)
115179
testee.userUpdatedQuery(BLANK_QUERY)
116180

117-
val newViewState = testee.viewState.value
181+
val newViewState = testee.resultsViewState.value
118182
assertNotNull(newViewState)
119183
assertTrue(newViewState!!.queryText.isEmpty())
120184
assertTrue(newViewState.appResults.isEmpty())
121185
assertEquals(AutoCompleteResult("", emptyList()), newViewState.autocompleteResults)
122186
}
123187

124188
@Test
125-
fun whenUserSubmitsQueryThenBrowserLaunched() {
189+
fun whenUserSubmitsQueryThenBrowserLaunchedAndPixelSent() {
126190
testee.userSubmittedQuery(QUERY)
127191
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
128192
assertEquals(Command.LaunchBrowser(QUERY), commandCaptor.lastValue)
193+
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY)
129194
}
130195

131196
@Test
132-
fun whenUserSubmitsAutocompleteResultThenBrowserLaunched() {
197+
fun whenUserSubmitsAutocompleteResultThenBrowserLaunchedAndPixelSent() {
133198
testee.userSubmittedAutocompleteResult(AUTOCOMPLETE_RESULT)
134199
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
135200
assertEquals(Command.LaunchBrowser(AUTOCOMPLETE_RESULT), commandCaptor.lastValue)
201+
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_BROWSER_QUERY)
136202
}
137203

138204
@Test
139-
fun whenUserSelectsAppResultThenAppLaunched() {
205+
fun whenUserSelectsAppResultThenAppLaunchedAndPixelSent() {
140206
testee.userSelectedApp(deviceApp)
141207
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
142208
assertEquals(Command.LaunchDeviceApplication(deviceApp), commandCaptor.lastValue)
209+
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_DEVICE_APP)
143210
}
144211

145212
@Test
146-
fun whenUserTapsDaxThenAppLaunched() {
213+
fun whenUserTapsDaxThenAppLaunchedAndPixelSent() {
147214
testee.userTappedDax()
148215
verify(commandObserver, atLeastOnce()).onChanged(commandCaptor.capture())
149216
assertTrue(commandCaptor.lastValue is LaunchDuckDuckGo)
217+
verify(mockPixel).fire(INTERSTITIAL_LAUNCH_DAX)
150218
}
151219

152220
@Test
@@ -157,6 +225,11 @@ class SystemSearchViewModelTest {
157225
assertEquals(Command.ShowAppNotFoundMessage(deviceApp.shortName), commandCaptor.lastValue)
158226
}
159227

228+
private fun whenOnboardingShowing() {
229+
whenever(mockOnboardingStore.shouldShow).thenReturn(true)
230+
testee.resetViewState()
231+
}
232+
160233
private fun ruleRunBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
161234
coroutineRule.testDispatcher.runBlockingTest(block)
162235

app/src/main/java/com/duckduckgo/app/global/ViewModelFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class ViewModelFactory @Inject constructor(
113113
with(modelClass) {
114114
when {
115115
isAssignableFrom(LaunchViewModel::class.java) -> LaunchViewModel(onboardingStore, appInstallationReferrerStateListener)
116-
isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(autoCompleteApi, deviceAppLookup)
116+
isAssignableFrom(SystemSearchViewModel::class.java) -> SystemSearchViewModel(onboardingStore, autoCompleteApi, deviceAppLookup, pixel)
117117
isAssignableFrom(OnboardingViewModel::class.java) -> onboardingViewModel()
118118
isAssignableFrom(BrowserViewModel::class.java) -> browserViewModel()
119119
isAssignableFrom(BrowserTabViewModel::class.java) -> browserTabViewModel()

app/src/main/java/com/duckduckgo/app/global/device/Device.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ interface DeviceInfo {
4343

4444
class ContextDeviceInfo @Inject constructor(private val context: Context) : DeviceInfo {
4545

46-
override val appVersion = "${BuildConfig.VERSION_NAME}"
46+
override val appVersion = BuildConfig.VERSION_NAME
4747

4848
override val majorAppVersion = appVersion.split(".").first()
4949

app/src/main/java/com/duckduckgo/app/global/view/ViewExtension.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,4 @@ fun View.hideKeyboard(): Boolean {
8080
}
8181

8282
fun Int.toDp(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
83-
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
83+
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()

app/src/main/java/com/duckduckgo/app/statistics/VariantManager.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,13 @@ interface VariantManager {
8484
// All groups in an experiment (control and variants) MUST use the same filters
8585
)
8686

87+
val REFERRER_VARIANTS = listOf(
88+
Variant(RESERVED_EU_AUCTION_VARIANT, features = listOf(SuppressHomeTabWidgetCta), filterBy = { noFilter() })
89+
)
90+
8791
fun referrerVariant(key: String): Variant {
88-
return Variant(key, features = emptyList(), filterBy = { noFilter() })
92+
val knownReferrer = REFERRER_VARIANTS.firstOrNull { it.key == key }
93+
return knownReferrer ?: Variant(key, features = emptyList(), filterBy = { noFilter() })
8994
}
9095

9196
private fun noFilter(): Boolean = true

app/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ interface Pixel {
100100
INTERSTITIAL_LAUNCH_BROWSER_QUERY(pixelName = "m_i_lbq"),
101101
INTERSTITIAL_LAUNCH_DEVICE_APP(pixelName = "m_i_sda"),
102102
INTERSTITIAL_LAUNCH_DAX(pixelName = "m_i_ld"),
103+
INTERSTITIAL_ONBOARDING_SHOWN(pixelName = "m_io_s"),
104+
INTERSTITIAL_ONBOARDING_DISMISSED(pixelName = "m_io_d"),
105+
INTERSTITIAL_ONBOARDING_LESS_PRESSED(pixelName = "m_io_l"),
106+
INTERSTITIAL_ONBOARDING_MORE_PRESSED(pixelName = "m_io_m"),
103107

104108
LONG_PRESS("mlp"),
105109
LONG_PRESS_DOWNLOAD_IMAGE("mlp_i"),

0 commit comments

Comments
 (0)