Skip to content

Commit

Permalink
Show on App Launch: Add additional tests (#5000)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1207908166761516/1208156273709083/f

### Description

Adds some additional tests for the BrowserViewModel and the
ShowOnAppLaunch store

### Steps to test this PR

N/A

### UI changes

N/A
  • Loading branch information
mikescamell authored Sep 20, 2024
1 parent 606fe1c commit 621d02e
Show file tree
Hide file tree
Showing 14 changed files with 531 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.pixels.AppPixelName
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_OFF
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_GENERAL_SETTINGS_TOGGLED_ON
import com.duckduckgo.app.pixels.AppPixelName.AUTOCOMPLETE_RECENT_SITES_GENERAL_SETTINGS_TOGGLED_OFF
Expand Down Expand Up @@ -143,6 +144,7 @@ class GeneralSettingsViewModel @Inject constructor(

fun onShowOnAppLaunchButtonClick() {
sendCommand(Command.LaunchShowOnAppLaunchScreen)
pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED)
}

private fun observeShowOnAppLaunchOption() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.generalsettings.showonapplaunch

import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.statistics.api.BrowserFeatureStateReporterPlugin
import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking

interface ShowOnAppLaunchReporterPlugin

@ContributesMultibinding(
scope = AppScope::class,
boundType = BrowserFeatureStateReporterPlugin::class,
)
@ContributesBinding(scope = AppScope::class, boundType = ShowOnAppLaunchReporterPlugin::class)
class ShowOnAppLaunchStateReporterPlugin
@Inject
constructor(
private val dispatcherProvider: DispatcherProvider,
private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore,
) : ShowOnAppLaunchReporterPlugin, BrowserFeatureStateReporterPlugin {

override fun featureStateParams(): Map<String, String> {
val option =
runBlocking(dispatcherProvider.io()) {
showOnAppLaunchOptionDataStore.optionFlow.first()
}
val dailyPixelValue = ShowOnAppLaunchOption.getDailyPixelValue(option)
return mapOf(PixelParameter.LAUNCH_SCREEN to dailyPixelValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
import com.duckduckgo.anvil.annotations.ContributesViewModel
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.ActivityScope
import javax.inject.Inject
Expand All @@ -38,6 +39,7 @@ class ShowOnAppLaunchViewModel @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore,
private val urlConverter: UrlConverter,
private val pixel: Pixel,
) : ViewModel() {

data class ViewState(
Expand Down Expand Up @@ -65,6 +67,7 @@ class ShowOnAppLaunchViewModel @Inject constructor(
fun onShowOnAppLaunchOptionChanged(option: ShowOnAppLaunchOption) {
Timber.i("User changed show on app launch option to $option")
viewModelScope.launch(dispatcherProvider.io()) {
firePixel(option)
showOnAppLaunchOptionDataStore.setShowOnAppLaunchOption(option)
}
}
Expand All @@ -76,4 +79,9 @@ class ShowOnAppLaunchViewModel @Inject constructor(
showOnAppLaunchOptionDataStore.setSpecificPageUrl(convertedUrl)
}
}

private fun firePixel(option: ShowOnAppLaunchOption) {
val pixelName = ShowOnAppLaunchOption.getPixelName(option)
pixel.fire(pixelName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@

package com.duckduckgo.app.generalsettings.showonapplaunch.model

import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED

sealed class ShowOnAppLaunchOption(val id: Int) {

data object LastOpenedTab : ShowOnAppLaunchOption(1)
Expand All @@ -30,5 +34,17 @@ sealed class ShowOnAppLaunchOption(val id: Int) {
3 -> SpecificPage("")
else -> throw IllegalArgumentException("Unknown id: $id")
}

fun getPixelName(option: ShowOnAppLaunchOption) = when (option) {
LastOpenedTab -> SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED
NewTabPage -> SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED
is SpecificPage -> SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED
}

fun getDailyPixelValue(option: ShowOnAppLaunchOption) = when (option) {
LastOpenedTab -> "last_opened_tab"
NewTabPage -> "new_tab_page"
is SpecificPage -> "specific_page"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.duckduckgo.app.generalsettings.showonapplaunch.store

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
Expand Down Expand Up @@ -72,17 +73,21 @@ class ShowOnAppLaunchOptionPrefsDataStore @Inject constructor(
preferences[intPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_OPTION)] = showOnAppLaunchOption.id

if (showOnAppLaunchOption is SpecificPage) {
preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL)]
preferences.setShowOnAppLaunch(showOnAppLaunchOption.url)
}
}
}

override suspend fun setSpecificPageUrl(url: String) {
store.edit { preferences ->
preferences[stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL)] = url
preferences.setShowOnAppLaunch(url)
}
}

private fun MutablePreferences.setShowOnAppLaunch(url: String) {
set(stringPreferencesKey(KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL), url)
}

companion object {
private const val KEY_SHOW_ON_APP_LAUNCH_OPTION = "SHOW_ON_APP_LAUNCH_OPTION"
private const val KEY_SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL = "SHOW_ON_APP_LAUNCH_SPECIFIC_PAGE_URL"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ enum class AppPixelName(override val pixelName: String) : Pixel.PixelName {
SETTINGS_PRIVATE_SEARCH_MORE_SEARCH_SETTINGS_PRESSED("ms_private_search_more_search_settings_pressed"),
SETTINGS_COOKIE_POPUP_PROTECTION_PRESSED("ms_cookie_popup_protection_setting_pressed"),
SETTINGS_FIRE_BUTTON_PRESSED("ms_fire_button_setting_pressed"),
SETTINGS_GENERAL_APP_LAUNCH_PRESSED("m_settings_general_app_launch_pressed"),
SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED("m_settings_general_app_launch_last_opened_tab_selected"),
SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED("m_settings_general_app_launch_new_tab_page_selected"),
SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED("m_settings_general_app_launch_specific_page_selected"),

SURVEY_CTA_SHOWN(pixelName = "mus_cs"),
SURVEY_CTA_DISMISSED(pixelName = "mus_cd"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.duckduckgo.app.browser.BrowserViewModel.Command
import com.duckduckgo.app.browser.defaultbrowsing.DefaultBrowserDetector
import com.duckduckgo.app.browser.omnibar.OmnibarEntryConverter
import com.duckduckgo.app.fire.DataClearer
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption
import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter
import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions
Expand All @@ -36,6 +37,7 @@ import com.duckduckgo.app.tabs.model.TabRepository
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory
import com.duckduckgo.feature.toggles.api.Toggle.State
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
Expand All @@ -53,37 +55,27 @@ class BrowserViewModelTest {
@Suppress("unused")
var instantTaskExecutorRule = InstantTaskExecutorRule()

@get:Rule
var coroutinesTestRule = CoroutineTestRule()
@get:Rule var coroutinesTestRule = CoroutineTestRule()

@Mock
private lateinit var mockCommandObserver: Observer<Command>
@Mock private lateinit var mockCommandObserver: Observer<Command>

private val commandCaptor = argumentCaptor<Command>()

@Mock
private lateinit var mockTabRepository: TabRepository
@Mock private lateinit var mockTabRepository: TabRepository

@Mock
private lateinit var mockOmnibarEntryConverter: OmnibarEntryConverter
@Mock private lateinit var mockOmnibarEntryConverter: OmnibarEntryConverter

@Mock
private lateinit var mockAutomaticDataClearer: DataClearer
@Mock private lateinit var mockAutomaticDataClearer: DataClearer

@Mock
private lateinit var mockAppEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder
@Mock private lateinit var mockAppEnjoymentUserEventRecorder: AppEnjoymentUserEventRecorder

@Mock
private lateinit var mockAppEnjoymentPromptEmitter: AppEnjoymentPromptEmitter
@Mock private lateinit var mockAppEnjoymentPromptEmitter: AppEnjoymentPromptEmitter

@Mock
private lateinit var mockPixel: Pixel
@Mock private lateinit var mockPixel: Pixel

@Mock
private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector
@Mock private lateinit var mockDefaultBrowserDetector: DefaultBrowserDetector

@Mock
private lateinit var showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore
@Mock private lateinit var showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore

private lateinit var testee: BrowserViewModel

Expand All @@ -97,18 +89,7 @@ class BrowserViewModelTest {

configureSkipUrlConversionInNewTabState(enabled = true)

testee = BrowserViewModel(
tabRepository = mockTabRepository,
queryUrlConverter = mockOmnibarEntryConverter,
dataClearer = mockAutomaticDataClearer,
appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter,
appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder,
defaultBrowserDetector = mockDefaultBrowserDetector,
dispatchers = coroutinesTestRule.testDispatcherProvider,
pixel = mockPixel,
skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature,
showOnAppLaunchOptionDataStore = showOnAppLaunchOptionDataStore,
)
initTestee()

testee.command.observeForever(mockCommandObserver)

Expand Down Expand Up @@ -272,6 +253,57 @@ class BrowserViewModelTest {
verify(mockOmnibarEntryConverter).convertQueryToUrl("query")
}

@Test
fun whenAppOpenAndLastOpenedTabSetThenNoTabsAdded() = runTest {
whenever(showOnAppLaunchOptionDataStore.optionFlow)
.thenReturn(flowOf(ShowOnAppLaunchOption.LastOpenedTab))

testee.handleShowOnAppLaunchOption()

verify(mockTabRepository, never()).add(url = any(), skipHome = any())
verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any())
verify(mockTabRepository, never()).addDefaultTab()
}

@Test
fun whenAppOpenAndNewTabPageSetThenNewTabAdded() = runTest {
whenever(showOnAppLaunchOptionDataStore.optionFlow)
.thenReturn(flowOf(ShowOnAppLaunchOption.NewTabPage))

testee.handleShowOnAppLaunchOption()

verify(mockTabRepository, atMost(1)).add()
verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any())
verify(mockTabRepository, never()).addDefaultTab()
}

@Test
fun whenAppOpenAndSpecificPageSetThenNewTabAddedWithUrl() = runTest {
whenever(showOnAppLaunchOptionDataStore.optionFlow)
.thenReturn(flowOf(ShowOnAppLaunchOption.SpecificPage("example.com")))

testee.handleShowOnAppLaunchOption()

verify(mockTabRepository, atMost(1)).add(url = "example.com", skipHome = false)
verify(mockTabRepository, never()).addFromSourceTab(url = any(), skipHome = any(), sourceTabId = any())
verify(mockTabRepository, never()).addDefaultTab()
}

private fun initTestee() {
testee = BrowserViewModel(
tabRepository = mockTabRepository,
queryUrlConverter = mockOmnibarEntryConverter,
dataClearer = mockAutomaticDataClearer,
appEnjoymentPromptEmitter = mockAppEnjoymentPromptEmitter,
appEnjoymentUserEventRecorder = mockAppEnjoymentUserEventRecorder,
defaultBrowserDetector = mockDefaultBrowserDetector,
dispatchers = coroutinesTestRule.testDispatcherProvider,
pixel = mockPixel,
skipUrlConversionOnNewTabFeature = skipUrlConversionOnNewTabFeature,
showOnAppLaunchOptionDataStore = showOnAppLaunchOptionDataStore,
)
}

private fun configureSkipUrlConversionInNewTabState(enabled: Boolean) {
skipUrlConversionOnNewTabFeature.self().setEnabled(State(enable = enabled))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchO
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage
import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage
import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLaunchOptionDataStore
import com.duckduckgo.app.pixels.AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED
import com.duckduckgo.app.statistics.pixels.Pixel
import com.duckduckgo.common.test.CoroutineTestRule
import com.duckduckgo.history.api.NavigationHistory
Expand Down Expand Up @@ -230,6 +231,13 @@ internal class GeneralSettingsViewModelTest {
}
}

@Test
fun whenShowOnAppLaunchClickedThenPixelFiredEmitted() = runTest {
testee.onShowOnAppLaunchButtonClick()

verify(mockPixel).fire(SETTINGS_GENERAL_APP_LAUNCH_PRESSED)
}

private fun defaultViewState() = GeneralSettingsViewModel.ViewState(
autoCompleteSuggestionsEnabled = true,
autoCompleteRecentlyVisitedSitesSuggestionsUserEnabled = true,
Expand Down
Loading

0 comments on commit 621d02e

Please sign in to comment.