diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt index aa8fd23901fc..fd85ef28f075 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -364,6 +364,8 @@ open class BrowserActivity : DuckDuckGoActivity() { return } } + + viewModel.handleShowOnAppLaunchOption() } private fun configureObservers() { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt index b9359b5f3586..7452b7b362f6 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -20,11 +20,16 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.duckduckgo.anvil.annotations.ContributesRemoteFeature import com.duckduckgo.anvil.annotations.ContributesViewModel 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.LastOpenedTab +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.ShowOnAppLaunchOptionDataStore import com.duckduckgo.app.global.ApplicationClearDataState import com.duckduckgo.app.global.rating.AppEnjoymentPromptEmitter import com.duckduckgo.app.global.rating.AppEnjoymentPromptOptions @@ -55,6 +60,7 @@ import com.duckduckgo.feature.toggles.api.Toggle import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import timber.log.Timber @@ -69,6 +75,7 @@ class BrowserViewModel @Inject constructor( private val dispatchers: DispatcherProvider, private val pixel: Pixel, private val skipUrlConversionOnNewTabFeature: SkipUrlConversionOnNewTabFeature, + private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, ) : ViewModel(), CoroutineScope { @@ -284,6 +291,21 @@ class BrowserViewModel @Inject constructor( fun onBookmarksActivityResult(url: String) { command.value = Command.OpenSavedSite(url) } + + fun handleShowOnAppLaunchOption() { + viewModelScope.launch { + when (val option = showOnAppLaunchOptionDataStore.optionFlow.first()) { + LastOpenedTab -> Unit + NewTabPage -> onNewTabRequested() + is SpecificPage -> { + val liveSelectedTabUrl = tabRepository.getSelectedTab()?.url + if (liveSelectedTabUrl != option.url) { + onOpenInNewTabRequested(option.url) + } + } + } + } + } } /** diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt index f737e87867e7..487bbd5ac77b 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModel.kt @@ -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 @@ -143,6 +144,7 @@ class GeneralSettingsViewModel @Inject constructor( fun onShowOnAppLaunchButtonClick() { sendCommand(Command.LaunchShowOnAppLaunchScreen) + pixel.fire(AppPixelName.SETTINGS_GENERAL_APP_LAUNCH_PRESSED) } private fun observeShowOnAppLaunchOption() { diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt new file mode 100644 index 000000000000..2bfcbbac34f7 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPlugin.kt @@ -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 { + val option = + runBlocking(dispatcherProvider.io()) { + showOnAppLaunchOptionDataStore.optionFlow.first() + } + val dailyPixelValue = ShowOnAppLaunchOption.getDailyPixelValue(option) + return mapOf(PixelParameter.LAUNCH_SCREEN to dailyPixelValue) + } +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt new file mode 100644 index 000000000000..15f4c86f42c2 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImpl.kt @@ -0,0 +1,45 @@ +/* + * 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 android.net.Uri +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore + +class ShowOnAppLaunchUrlConverterImpl : UrlConverter { + + override fun convertUrl(url: String?): String { + if (url.isNullOrBlank()) return ShowOnAppLaunchOptionDataStore.DEFAULT_SPECIFIC_PAGE_URL + + val uri = Uri.parse(url.trim()) + + val convertedUri = if (uri.scheme == null) { + Uri.Builder().scheme("http").authority(uri.path?.lowercase()) + } else { + uri.buildUpon() + .scheme(uri.scheme?.lowercase()) + .authority(uri.authority?.lowercase()) + } + .apply { + query(uri.query) + fragment(uri.fragment) + } + .build() + .toString() + + return Uri.decode(convertedUri) + } +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt index 7b1a3d6aac09..965d3cdc3dcc 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModel.kt @@ -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 @@ -37,6 +38,8 @@ import timber.log.Timber class ShowOnAppLaunchViewModel @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val showOnAppLaunchOptionDataStore: ShowOnAppLaunchOptionDataStore, + private val urlConverter: UrlConverter, + private val pixel: Pixel, ) : ViewModel() { data class ViewState( @@ -64,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) } } @@ -71,7 +75,13 @@ class ShowOnAppLaunchViewModel @Inject constructor( fun setSpecificPageUrl(url: String) { Timber.i("Setting specific page url to $url") viewModelScope.launch(dispatcherProvider.io()) { - showOnAppLaunchOptionDataStore.setSpecificPageUrl(url) + val convertedUrl = urlConverter.convertUrl(url) + showOnAppLaunchOptionDataStore.setSpecificPageUrl(convertedUrl) } } + + private fun firePixel(option: ShowOnAppLaunchOption) { + val pixelName = ShowOnAppLaunchOption.getPixelName(option) + pixel.fire(pixelName) + } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt new file mode 100644 index 000000000000..87703ab36a73 --- /dev/null +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/UrlConverter.kt @@ -0,0 +1,22 @@ +/* + * 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 + +interface UrlConverter { + + fun convertUrl(url: String?): String +} diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt index fdc384e94a02..806794a2df61 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/model/ShowOnAppLaunchOption.kt @@ -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) @@ -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" + } } } diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt index 6ad3493c006a..291efa1fe04a 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchDataStoreModule.kt @@ -20,6 +20,8 @@ import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.ShowOnAppLaunchUrlConverterImpl +import com.duckduckgo.app.generalsettings.showonapplaunch.UrlConverter import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesTo import dagger.Module @@ -37,6 +39,9 @@ object ShowOnAppLaunchDataStoreModule { @Provides @ShowOnAppLaunch fun showOnAppLaunchDataStore(context: Context): DataStore = context.showOnAppLaunchDataStore + + @Provides + fun showOnAppLaunchUrlConverter(): UrlConverter = ShowOnAppLaunchUrlConverterImpl() } @Qualifier diff --git a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt index 25fae184a2b7..298800165e4e 100644 --- a/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt +++ b/app/src/main/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchOptionDataStore.kt @@ -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 @@ -25,6 +26,7 @@ import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchO import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab 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.ShowOnAppLaunchOptionDataStore.Companion.DEFAULT_SPECIFIC_PAGE_URL import com.duckduckgo.di.scopes.AppScope import com.squareup.anvil.annotations.ContributesBinding import javax.inject.Inject @@ -37,6 +39,10 @@ interface ShowOnAppLaunchOptionDataStore { suspend fun setShowOnAppLaunchOption(showOnAppLaunchOption: ShowOnAppLaunchOption) suspend fun setSpecificPageUrl(url: String) + + companion object { + const val DEFAULT_SPECIFIC_PAGE_URL = "https://duckduckgo.com/" + } } @ContributesBinding(AppScope::class) @@ -67,19 +73,22 @@ 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 DEFAULT_SPECIFIC_PAGE_URL = "duckduckgo.com" 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" } diff --git a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt index d43daef4b3d5..09fa7f3e333c 100644 --- a/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt +++ b/app/src/main/java/com/duckduckgo/app/pixels/AppPixelName.kt @@ -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"), diff --git a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt index 70f08a2dff60..f34550357e0f 100644 --- a/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt +++ b/app/src/main/java/com/duckduckgo/app/tabs/model/TabDataRepository.kt @@ -298,6 +298,9 @@ class TabDataRepository @Inject constructor( siteData.clear() } + override suspend fun getSelectedTab(): TabEntity? = + withContext(dispatchers.io()) { tabsDao.selectedTab() } + override suspend fun select(tabId: String) { databaseExecutor().scheduleDirect { val selection = TabSelectionEntity(tabId = tabId) diff --git a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt index f660ae9b8b3c..836fb0c1c20f 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/BrowserViewModelTest.kt @@ -23,6 +23,8 @@ 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 import com.duckduckgo.app.global.rating.AppEnjoymentUserEventRecorder @@ -35,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 @@ -52,34 +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 + @Mock private lateinit var mockCommandObserver: Observer private val commandCaptor = argumentCaptor() - @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 private lateinit var testee: BrowserViewModel @@ -93,17 +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, - ) + initTestee() testee.command.observeForever(mockCommandObserver) @@ -267,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)) } diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt index a592ce39533a..8058465395bb 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/GeneralSettingsViewModelTest.kt @@ -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 @@ -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, diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt new file mode 100644 index 000000000000..07c10d5e99a3 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchStateReporterPluginTest.kt @@ -0,0 +1,70 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption +import com.duckduckgo.app.generalsettings.showonapplaunch.store.FakeShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore +import com.duckduckgo.app.statistics.pixels.Pixel.PixelParameter +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchReporterPluginTest { + + @get:Rule val coroutineTestRule = CoroutineTestRule() + + private val dispatcherProvider: DispatcherProvider = coroutineTestRule.testDispatcherProvider + private lateinit var testee: ShowOnAppLaunchStateReporterPlugin + private lateinit var fakeDataStore: ShowOnAppLaunchOptionDataStore + + @Before + fun setup() { + fakeDataStore = FakeShowOnAppLaunchOptionDataStore(ShowOnAppLaunchOption.LastOpenedTab) + + testee = ShowOnAppLaunchStateReporterPlugin(dispatcherProvider, fakeDataStore) + } + + @Test + fun whenOptionIsSetToLastOpenedPageThenShouldReturnDailyPixelValue() = runTest { + fakeDataStore.setShowOnAppLaunchOption(ShowOnAppLaunchOption.LastOpenedTab) + val result = testee.featureStateParams() + assertEquals("last_opened_tab", result[PixelParameter.LAUNCH_SCREEN]) + } + + @Test + fun whenOptionIsSetToNewTabPageThenShouldReturnDailyPixelValue() = runTest { + fakeDataStore.setShowOnAppLaunchOption(ShowOnAppLaunchOption.NewTabPage) + val result = testee.featureStateParams() + assertEquals("new_tab_page", result[PixelParameter.LAUNCH_SCREEN]) + } + + @Test + fun whenOptionIsSetToSpecificPageThenShouldReturnDailyPixelValue() = runTest { + val specificPage = ShowOnAppLaunchOption.SpecificPage("example.com") + fakeDataStore.setShowOnAppLaunchOption(specificPage) + val result = testee.featureStateParams() + assertEquals("specific_page", result[PixelParameter.LAUNCH_SCREEN]) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt new file mode 100644 index 000000000000..ed3109a79593 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchUrlConverterImplTest.kt @@ -0,0 +1,125 @@ +/* + * 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 androidx.test.ext.junit.runners.AndroidJUnit4 +import com.duckduckgo.app.generalsettings.showonapplaunch.store.ShowOnAppLaunchOptionDataStore.Companion.DEFAULT_SPECIFIC_PAGE_URL +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchUrlConverterImplTest { + + private val urlConverter = ShowOnAppLaunchUrlConverterImpl() + + @Test + fun whenUrlIsNullThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl(null) + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlIsEmptyThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl("") + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlIsBlankThenShouldReturnDefaultUrl() { + val result = urlConverter.convertUrl(" ") + assertEquals(DEFAULT_SPECIFIC_PAGE_URL, result) + } + + @Test + fun whenUrlHasNoSchemeThenHttpSchemeIsAdded() { + val result = urlConverter.convertUrl("www.example.com") + assertEquals("http://www.example.com", result) + } + + @Test + fun whenUrlHasNoSchemeAndSubdomainThenHttpSchemeIsAdded() { + val result = urlConverter.convertUrl("example.com") + assertEquals("http://example.com", result) + } + + @Test + fun whenUrlHasASchemeThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com") + assertEquals("https://www.example.com", result) + } + + @Test + fun whenUrlHasDifferentSchemeThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("ftp://www.example.com") + assertEquals("ftp://www.example.com", result) + } + + @Test + fun whenUrlHasSpecialCharactersThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com/path?query=param&another=param") + assertEquals("https://www.example.com/path?query=param&another=param", result) + } + + @Test + fun whenUrlHasPortThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com:8080") + assertEquals("https://www.example.com:8080", result) + } + + @Test + fun whenUrlHasPathAndQueryParametersThenShouldReturnTheSameUrl() { + val result = urlConverter.convertUrl("https://www.example.com/path/to/resource?query=param") + assertEquals("https://www.example.com/path/to/resource?query=param", result) + } + + @Test + fun whenUrlHasUppercaseProtocolThenShouldLowercaseProtocol() { + val result = urlConverter.convertUrl("HTTPS://www.example.com") + assertEquals("https://www.example.com", result) + } + + @Test + fun whenUrlHasUppercaseSubdomainThenShouldLowercaseSubdomain() { + val result = urlConverter.convertUrl("https://WWW.example.com") + assertEquals("https://www.example.com", result) + } + + @Test + fun whenUrlHasUppercaseDomainThenShouldLowercaseDomain() { + val result = urlConverter.convertUrl("https://www.EXAMPLE.com") + assertEquals("https://www.example.com", result) + } + + @Test + fun whenUrlHasUppercaseTopLevelDomainThenShouldLowercaseTopLevelDomain() { + val result = urlConverter.convertUrl("https://www.example.COM") + assertEquals("https://www.example.com", result) + } + + @Test + fun whenUrlHasMixedCaseThenOnlyProtocolSubdomainDomainAndTldAreLowercased() { + val result = urlConverter.convertUrl("HTTPS://WWW.EXAMPLE.COM/Path?Query=Param#Fragment") + assertEquals("https://www.example.com/Path?Query=Param#Fragment", result) + } + + @Test + fun whenUrlIsNotAValidUrlReturnsInvalidUrlWithHttpScheme() { + val result = urlConverter.convertUrl("example") + assertEquals("http://example", result) + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt new file mode 100644 index 000000000000..dc8a0af55ac0 --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/ShowOnAppLaunchViewModelTest.kt @@ -0,0 +1,120 @@ +/* + * 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 app.cash.turbine.test +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab +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_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 +import com.duckduckgo.common.test.CoroutineTestRule +import com.duckduckgo.common.utils.DispatcherProvider +import com.duckduckgo.fakes.FakePixel +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class ShowOnAppLaunchViewModelTest { + + @get:Rule + val coroutineTestRule = CoroutineTestRule() + + private lateinit var testee: ShowOnAppLaunchViewModel + private lateinit var fakeDataStore: FakeShowOnAppLaunchOptionDataStore + private lateinit var fakePixel: FakePixel + private val dispatcherProvider: DispatcherProvider = coroutineTestRule.testDispatcherProvider + + @Before + fun setup() { + fakeDataStore = FakeShowOnAppLaunchOptionDataStore(LastOpenedTab) + fakePixel = FakePixel() + testee = ShowOnAppLaunchViewModel(dispatcherProvider, fakeDataStore, FakeUrlConverter(), fakePixel) + } + + @Test + fun whenViewModelInitializedThenInitialStateIsCorrect() = runTest { + testee.viewState.test { + val initialState = awaitItem() + assertEquals(LastOpenedTab, initialState.selectedOption) + assertEquals("https://duckduckgo.com", initialState.specificPageUrl) + } + } + + @Test + fun whenShowOnAppLaunchOptionChangedThenStateIsUpdated() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(NewTabPage, updatedState.selectedOption) + } + } + + @Test + fun whenSpecificPageUrlSetThenStateIsUpdated() = runTest { + val newUrl = "https://example.com" + + testee.setSpecificPageUrl(newUrl) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(newUrl, updatedState.specificPageUrl) + } + } + + @Test + fun whenMultipleOptionsChangedThenStateIsUpdatedCorrectly() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.onShowOnAppLaunchOptionChanged(LastOpenedTab) + testee.viewState.test { + val updatedState = awaitItem() + assertEquals(LastOpenedTab, updatedState.selectedOption) + } + } + + @Test + fun whenOptionChangedToLastOpenedPageThenLastOpenedPageIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + testee.onShowOnAppLaunchOptionChanged(LastOpenedTab) + assertEquals(2, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_LAST_OPENED_TAB_SELECTED.pixelName, fakePixel.firedPixels.last()) + } + + @Test + fun whenOptionChangedToNewTabPageThenNewTabPagePixelIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(NewTabPage) + assertEquals(1, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_NEW_TAB_PAGE_SELECTED.pixelName, fakePixel.firedPixels[0]) + } + + @Test + fun whenOptionChangedToSpecificPageThenSpecificPixelIsFired() = runTest { + testee.onShowOnAppLaunchOptionChanged(SpecificPage("https://example.com")) + assertEquals(1, fakePixel.firedPixels.size) + assertEquals(SETTINGS_GENERAL_APP_LAUNCH_SPECIFIC_PAGE_SELECTED.pixelName, fakePixel.firedPixels[0]) + } + + private class FakeUrlConverter : UrlConverter { + + override fun convertUrl(url: String?): String { + return url ?: "https://duckduckgo.com" + } + } +} diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt index 51261a3d6e70..d96d1c396cba 100644 --- a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/FakeShowOnAppLaunchOptionDataStore.kt @@ -22,11 +22,11 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.filterNotNull -class FakeShowOnAppLaunchOptionDataStore : ShowOnAppLaunchOptionDataStore { +class FakeShowOnAppLaunchOptionDataStore(defaultOption: ShowOnAppLaunchOption? = null) : ShowOnAppLaunchOptionDataStore { - private var currentOptionStateFlow = MutableStateFlow(null) + private var currentOptionStateFlow = MutableStateFlow(defaultOption) - private var currentSpecificPageUrl = MutableStateFlow("duckduckgo.com") + private var currentSpecificPageUrl = MutableStateFlow("https://duckduckgo.com") override val optionFlow: Flow = currentOptionStateFlow.asStateFlow().filterNotNull() diff --git a/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt new file mode 100644 index 000000000000..dcb1c8dec27a --- /dev/null +++ b/app/src/test/java/com/duckduckgo/app/generalsettings/showonapplaunch/store/ShowOnAppLaunchPrefsDataStoreTest.kt @@ -0,0 +1,115 @@ +/* + * 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.store + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.LastOpenedTab +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.NewTabPage +import com.duckduckgo.app.generalsettings.showonapplaunch.model.ShowOnAppLaunchOption.SpecificPage +import com.duckduckgo.common.test.CoroutineTestRule +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ShowOnAppLaunchPrefsDataStoreTest { + + @get:Rule val coroutineRule = CoroutineTestRule() + + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private val dataStoreFile = context.preferencesDataStoreFile("show_on_app_launch") + + private val testDataStore: DataStore = + PreferenceDataStoreFactory.create( + scope = coroutineRule.testScope, + produceFile = { dataStoreFile }, + ) + + private val testee: ShowOnAppLaunchOptionDataStore = + ShowOnAppLaunchOptionPrefsDataStore(testDataStore) + + @After + fun after() { + dataStoreFile.delete() + } + + @Test + fun whenOptionIsNullThenShouldReturnLastOpenedPage() = runTest { + assertEquals(LastOpenedTab, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToLastOpenedPageThenShouldReturnLastOpenedPage() = runTest { + testee.setShowOnAppLaunchOption(LastOpenedTab) + assertEquals(LastOpenedTab, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToNewTabPageThenShouldReturnNewTabPage() = runTest { + testee.setShowOnAppLaunchOption(NewTabPage) + assertEquals(NewTabPage, testee.optionFlow.first()) + } + + @Test + fun whenOptionIsSetToSpecificPageThenShouldReturnSpecificPage() = runTest { + val specificPage = SpecificPage("example.com") + + testee.setShowOnAppLaunchOption(specificPage) + assertEquals(specificPage, testee.optionFlow.first()) + } + + @Test + fun whenSpecificPageIsNullThenShouldReturnDefaultUrl() = runTest { + assertEquals("https://duckduckgo.com/", testee.specificPageUrlFlow.first()) + } + + @Test + fun whenSpecificPageUrlIsSetThenShouldReturnSpecificPageUrl() = runTest { + testee.setSpecificPageUrl("example.com") + assertEquals("example.com", testee.specificPageUrlFlow.first()) + } + + @Test + fun whenOptionIsChangedThenNewOptionEmitted() = runTest { + testee.optionFlow.test { + val defaultOption = awaitItem() + + assertEquals(LastOpenedTab, defaultOption) + + testee.setShowOnAppLaunchOption(NewTabPage) + + assertEquals(NewTabPage, awaitItem()) + + testee.setShowOnAppLaunchOption(SpecificPage("example.com")) + + assertEquals(SpecificPage("example.com"), awaitItem()) + } + } +} diff --git a/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt b/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt new file mode 100644 index 000000000000..ca4bbb7ec52d --- /dev/null +++ b/app/src/test/java/com/duckduckgo/fakes/FakePixel.kt @@ -0,0 +1,60 @@ +/* + * 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.fakes + +import com.duckduckgo.app.statistics.pixels.Pixel +import com.duckduckgo.app.statistics.pixels.Pixel.PixelName +import com.duckduckgo.app.statistics.pixels.Pixel.PixelType + +internal class FakePixel : Pixel { + + val firedPixels = mutableListOf() + + override fun fire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun fire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + type: PixelType, + ) { + firedPixels.add(pixelName) + } + + override fun enqueueFire( + pixel: PixelName, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixel.pixelName) + } + + override fun enqueueFire( + pixelName: String, + parameters: Map, + encodedParameters: Map, + ) { + firedPixels.add(pixelName) + } +} diff --git a/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt b/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt index a96eb9f44eb5..d9adb1780081 100644 --- a/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt +++ b/browser-api/src/main/java/com/duckduckgo/app/tabs/model/TabRepository.kt @@ -93,6 +93,8 @@ interface TabRepository { suspend fun deleteAll() + suspend fun getSelectedTab(): TabEntity? + suspend fun select(tabId: String) fun updateTabPreviewImage( diff --git a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt index c0fb603e92a4..f7776e936252 100644 --- a/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt +++ b/statistics/statistics-api/src/main/java/com/duckduckgo/app/statistics/pixels/Pixel.kt @@ -60,6 +60,7 @@ interface Pixel { // Loading Bar Experiment const val LOADING_BAR_EXPERIMENT = "loading_bar_exp" + const val LAUNCH_SCREEN = "launch_screen" } object PixelValues {