From 5b8dd051654d5f05d272ecbc57ae24a421355510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Fri, 14 Nov 2025 16:46:06 +0100 Subject: [PATCH 1/8] Duck.ai: Tabs lifecycle --- .../duckduckgo/app/browser/BrowserTabViewModel.kt | 14 +++++++++----- .../java/com/duckduckgo/duckchat/api/DuckChat.kt | 5 +++++ .../com/duckduckgo/duckchat/impl/RealDuckChat.kt | 7 +++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 2866abdda9a8..a1a7c89f10cd 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -4470,7 +4470,8 @@ class BrowserTabViewModel @Inject constructor( val params = duckChat.createWasUsedBeforePixelParams() pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, parameters = params) } - duckChat.openDuckChat() + val url = duckChat.getDuckChatUrl("", false) + onUserSubmittedQuery(url) } fun onDuckChatOmnibarButtonClicked( @@ -4481,11 +4482,14 @@ class BrowserTabViewModel @Inject constructor( viewModelScope.launch { command.value = HideKeyboardForChat } - when { - hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat() - hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "") - else -> duckChat.openDuckChat() + + val url = when { + hasFocus && isNtp && query.isNullOrBlank() -> duckChat.getDuckChatUrl(query ?: "", false) + hasFocus -> duckChat.getDuckChatUrl(query ?: "", true) + else -> duckChat.getDuckChatUrl(query ?: "", false) } + + onUserSubmittedQuery(url) } fun onVpnMenuClicked() { diff --git a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt index d61c6f1bcfb1..e4dc75d84b38 100644 --- a/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt +++ b/duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt @@ -46,6 +46,11 @@ interface DuckChat { */ fun openDuckChatWithPrefill(query: String) + /** + * Returns the Duck Chat URL to be used + */ + fun getDuckChatUrl(query: String, autoPrompt: Boolean): String + /** * Determines whether a given [Uri] is a DuckChat URL. * diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt index 79c88dffd1f8..c66843ebbaf1 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/RealDuckChat.kt @@ -481,6 +481,13 @@ class RealDuckChat @Inject constructor( openDuckChat(parameters, forceNewSession = true) } + override fun getDuckChatUrl(query: String, autoPrompt: Boolean): String { + logcat { "Duck.ai: getDuckChatUrl query $query autoPrompt $autoPrompt" } + val parameters = addChatParameters(query, autoPrompt = autoPrompt) + val url = appendParameters(parameters, duckChatLink) + return url + } + private fun addChatParameters( query: String, autoPrompt: Boolean, From 8901d00e13f51583a9d09caae8e6c5134bdd39e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 09:49:44 +0100 Subject: [PATCH 2/8] clean up tabs --- .../duckduckgo/app/browser/BrowserActivity.kt | 24 +++++------ .../app/browser/BrowserTabViewModel.kt | 40 ++++++++++++++----- .../app/browser/BrowserViewModel.kt | 4 +- .../app/browser/BrowserWebViewClient.kt | 1 + .../app/tabs/model/TabDataRepository.kt | 1 + 5 files changed, 43 insertions(+), 27 deletions(-) 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 4ae7c4642ed1..0c92c51cd8ec 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt @@ -743,7 +743,6 @@ open class BrowserActivity : DuckDuckGoActivity() { command.duckChatSessionActive, command.withTransition, command.tabs, - command.fullScreenMode, ) Command.LaunchTabSwitcher -> currentTab?.launchTabSwitcherAfterTabsUndeleted() @@ -837,24 +836,19 @@ open class BrowserActivity : DuckDuckGoActivity() { duckChatSessionActive: Boolean, withTransition: Boolean, tabs: Int, - fullScreenMode: Boolean, ) { - if (fullScreenMode) { - currentTab?.submitQuery(url!!) - } else { - duckAiFragment?.let { fragment -> - if (duckChatSessionActive) { - restoreDuckChat(fragment, withTransition) - } else { - launchNewDuckChat(url, withTransition, tabs) - } - } ?: run { + duckAiFragment?.let { fragment -> + if (duckChatSessionActive) { + restoreDuckChat(fragment, withTransition) + } else { launchNewDuckChat(url, withTransition, tabs) } + } ?: run { + launchNewDuckChat(url, withTransition, tabs) + } - currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let { - hideKeyboard(it) - } + currentTab?.getOmnibar()?.omnibarView?.omnibarTextInput?.let { + hideKeyboard(it) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index a1a7c89f10cd..fff6ae4120f0 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1079,6 +1079,7 @@ class BrowserTabViewModel @Inject constructor( query: String, queryOrigin: QueryOrigin = QueryOrigin.FromUser, ) { + logcat { "Duck.ai: onUserSubmittedQuery $query" } navigationAwareLoginDetector.onEvent(NavigationEvent.UserAction.NewQuerySubmitted) if (query.isBlank()) { @@ -1132,14 +1133,18 @@ class BrowserTabViewModel @Inject constructor( val verticalParameter = extractVerticalParameter(url) var urlToNavigate = queryUrlConverter.convertQueryToUrl(trimmedInput, verticalParameter, queryOrigin) + logcat { "Duck.ai: urlToNavigate $urlToNavigate" } + when (val type = specialUrlDetector.determineType(trimmedInput)) { is ShouldLaunchDuckChatLink -> { runCatching { if (duckAiFeatureState.showFullScreenMode.value) { + logcat { "Duck.ai: ShouldLaunchDuckChatLink $urlToNavigate" } site?.nextUrl = urlToNavigate command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) } else { val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY) + logcat { "Duck.ai: ShouldLaunchDuckChatLink queryParameter $queryParameter" } if (queryParameter != null) { duckChat.openDuckChatWithPrefill(queryParameter) } else { @@ -1544,6 +1549,7 @@ class BrowserTabViewModel @Inject constructor( when (stateChange) { is WebNavigationStateChange.NewPage -> { + logcat { "Duck.ai: WebNavigationStateChange.NewPage ${stateChange.url.toUri()}" } val uri = stateChange.url.toUri() viewModelScope.launch(dispatchers.io()) { if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) { @@ -1569,6 +1575,7 @@ class BrowserTabViewModel @Inject constructor( is WebNavigationStateChange.PageCleared -> pageCleared() is WebNavigationStateChange.UrlUpdated -> { + logcat { "Duck.ai: urlUpdated ${stateChange.url}" } val uri = stateChange.url.toUri() viewModelScope.launch(dispatchers.io()) { if (duckPlayer.getDuckPlayerState() == ENABLED && duckPlayer.isSimulatedYoutubeNoCookie(uri)) { @@ -1626,7 +1633,7 @@ class BrowserTabViewModel @Inject constructor( url: String, title: String?, ) { - logcat(VERBOSE) { "Page changed: $url" } + logcat(VERBOSE) { "Duck.ai: Page changed: $url" } cleanupBlobDownloadReplyProxyMaps() hasCtaBeenShownForCurrentPage.set(false) @@ -1899,6 +1906,7 @@ class BrowserTabViewModel @Inject constructor( } override fun pageRefreshed(refreshedUrl: String) { + logcat { "Duck.ai pageRefreshed URL: $url refreshedUrl $refreshedUrl" } if (url == null || refreshedUrl == url) { logcat(VERBOSE) { "Page refreshed: $refreshedUrl" } pageChanged(refreshedUrl, title) @@ -2185,6 +2193,7 @@ class BrowserTabViewModel @Inject constructor( } private fun onSiteChanged() { + logcat { "Duck.ai: onSiteChanged" } httpsUpgraded = false site?.isDesktopMode = currentBrowserViewState().isDesktopBrowsingMode viewModelScope.launch { @@ -4470,8 +4479,14 @@ class BrowserTabViewModel @Inject constructor( val params = duckChat.createWasUsedBeforePixelParams() pixel.fire(DuckChatPixelName.DUCK_CHAT_OPEN_BROWSER_MENU, parameters = params) } - val url = duckChat.getDuckChatUrl("", false) - onUserSubmittedQuery(url) + + if (duckAiFeatureState.showFullScreenMode.value) { + val url = duckChat.getDuckChatUrl("", false) + pageRefreshed(url) + onUserSubmittedQuery(url) + } else { + duckChat.openDuckChat() + } } fun onDuckChatOmnibarButtonClicked( @@ -4483,13 +4498,20 @@ class BrowserTabViewModel @Inject constructor( command.value = HideKeyboardForChat } - val url = when { - hasFocus && isNtp && query.isNullOrBlank() -> duckChat.getDuckChatUrl(query ?: "", false) - hasFocus -> duckChat.getDuckChatUrl(query ?: "", true) - else -> duckChat.getDuckChatUrl(query ?: "", false) + if (duckAiFeatureState.showFullScreenMode.value) { + val url = when { + hasFocus && isNtp && query.isNullOrBlank() -> duckChat.getDuckChatUrl(query ?: "", false) + hasFocus -> duckChat.getDuckChatUrl(query ?: "", true) + else -> duckChat.getDuckChatUrl(query ?: "", false) + } + onUserSubmittedQuery(url) + } else { + when { + hasFocus && isNtp && query.isNullOrBlank() -> duckChat.openDuckChat() + hasFocus -> duckChat.openDuckChatWithAutoPrompt(query ?: "") + else -> duckChat.openDuckChat() + } } - - onUserSubmittedQuery(url) } fun onVpnMenuClicked() { 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 6acdc1d6bfcf..a0dc0940bc29 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserViewModel.kt @@ -144,7 +144,6 @@ class BrowserViewModel @Inject constructor( val duckChatSessionActive: Boolean, val withTransition: Boolean, val tabs: Int, - val fullScreenMode: Boolean, ) : Command() } @@ -507,10 +506,9 @@ class BrowserViewModel @Inject constructor( duckChatSessionActive: Boolean, withTransition: Boolean, ) { - val duckAiFullScreenMode = duckAiFeatureState.showFullScreenMode.value logcat(INFO) { "Duck.ai openDuckChat duckChatSessionActive $duckChatSessionActive" } val tabsCount = tabs.value?.size ?: 0 - sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount, duckAiFullScreenMode)) + sendCommand(OpenDuckChat(duckChatUrl, duckChatSessionActive, withTransition, tabsCount)) } } diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt index e700b925baff..b5ca99f9588e 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserWebViewClient.kt @@ -470,6 +470,7 @@ class BrowserWebViewClient @Inject constructor( url: String?, favicon: Bitmap?, ) { + logcat { "Duck.ai onPageStarted webViewUrl: ${webView.url} URL: $url lastPageStarted $lastPageStarted" } url?.let { // See https://app.asana.com/0/0/1206159443951489/f (WebView limitations) if (it != ABOUT_BLANK && start == null) { 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 bc100c2c49ea..2dcb2843ecd8 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 @@ -287,6 +287,7 @@ class TabDataRepository @Inject constructor( site: Site?, ) { databaseExecutor().scheduleDirect { + logcat { "Duck.ai: updateUrlAndTitle url: ${site?.url} title: ${site?.title}" } tabsDao.updateUrlAndTitle(tabId, site?.url, site?.title, viewed = true) } } From 597df19261cf0396ee0fc594fb8b275ac66f733f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 13:27:37 +0100 Subject: [PATCH 3/8] fix tab livecycle in non input screen mode --- .../app/browser/BrowserTabViewModel.kt | 16 +++++----------- .../duckduckgo/app/browser/SpecialUrlDetector.kt | 4 +++- .../duckduckgo/app/browser/di/BrowserModule.kt | 3 +++ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index fff6ae4120f0..8d6e8569c458 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -1138,18 +1138,12 @@ class BrowserTabViewModel @Inject constructor( when (val type = specialUrlDetector.determineType(trimmedInput)) { is ShouldLaunchDuckChatLink -> { runCatching { - if (duckAiFeatureState.showFullScreenMode.value) { - logcat { "Duck.ai: ShouldLaunchDuckChatLink $urlToNavigate" } - site?.nextUrl = urlToNavigate - command.value = NavigationCommand.Navigate(urlToNavigate, getUrlHeaders(urlToNavigate)) + logcat { "Duck.ai: ShouldLaunchDuckChatLink $urlToNavigate" } + val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY) + if (queryParameter != null) { + duckChat.openDuckChatWithPrefill(queryParameter) } else { - val queryParameter = urlToNavigate.toUri().getQueryParameter(QUERY) - logcat { "Duck.ai: ShouldLaunchDuckChatLink queryParameter $queryParameter" } - if (queryParameter != null) { - duckChat.openDuckChatWithPrefill(queryParameter) - } else { - duckChat.openDuckChat() - } + duckChat.openDuckChat() } return } diff --git a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt index d51efd538bd2..bc07eebcc1b8 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt @@ -30,6 +30,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetector.UrlType import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.duckchat.api.DuckAiFeatureState import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.privacy.config.api.AmpLinkType @@ -49,6 +50,7 @@ class SpecialUrlDetectorImpl( private val externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature, private val duckPlayer: DuckPlayer, private val duckChat: DuckChat, + private val duckAiFeatureState: DuckAiFeatureState, private val aiChatQueryDetectionFeature: AIChatQueryDetectionFeature, private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, ) : SpecialUrlDetector { @@ -107,7 +109,7 @@ class SpecialUrlDetectorImpl( val uri = uriString.toUri() - if (duckChat.isDuckChatUrl(uri)) { + if (duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value) { return UrlType.ShouldLaunchDuckChatLink } diff --git a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt index f068e06af9f9..fe76c4419762 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/di/BrowserModule.kt @@ -84,6 +84,7 @@ import com.duckduckgo.downloads.api.FileDownloader import com.duckduckgo.downloads.impl.AndroidFileDownloader import com.duckduckgo.downloads.impl.DataUriDownloader import com.duckduckgo.downloads.impl.FileDownloadCallback +import com.duckduckgo.duckchat.api.DuckAiFeatureState import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.experiments.api.VariantManager @@ -193,6 +194,7 @@ class BrowserModule { externalAppIntentFlagsFeature: ExternalAppIntentFlagsFeature, duckPlayer: DuckPlayer, duckChat: DuckChat, + duckChaFeatureState: DuckAiFeatureState, aiChatQueryDetectionFeature: AIChatQueryDetectionFeature, androidBrowserConfigFeature: AndroidBrowserConfigFeature, ): SpecialUrlDetector = SpecialUrlDetectorImpl( @@ -203,6 +205,7 @@ class BrowserModule { externalAppIntentFlagsFeature, duckPlayer, duckChat, + duckChaFeatureState, aiChatQueryDetectionFeature, androidBrowserConfigFeature, ) From ca0f36e4cab3339a5101802e637baeac8854ecda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 14:12:51 +0100 Subject: [PATCH 4/8] fix tab lifecycle for input screen --- .../ui/state/InputScreenVisibilityState.kt | 19 +++++++++--------- .../ui/viewmodel/InputScreenViewModel.kt | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/state/InputScreenVisibilityState.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/state/InputScreenVisibilityState.kt index d8041d990ade..c48d0e36f204 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/state/InputScreenVisibilityState.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/state/InputScreenVisibilityState.kt @@ -17,15 +17,16 @@ package com.duckduckgo.duckchat.impl.inputscreen.ui.state data class InputScreenVisibilityState( - val submitButtonVisible: Boolean, - val voiceInputButtonVisible: Boolean, - val autoCompleteSuggestionsVisible: Boolean, - val bottomFadeVisible: Boolean, - val showChatLogo: Boolean, - val showSearchLogo: Boolean, - val newLineButtonVisible: Boolean, - val mainButtonsVisible: Boolean, - val searchMode: Boolean, + val submitButtonVisible: Boolean = false, + val voiceInputButtonVisible: Boolean = false, + val autoCompleteSuggestionsVisible: Boolean = false, + val bottomFadeVisible: Boolean = false, + val showChatLogo: Boolean = true, + val showSearchLogo: Boolean = true, + val newLineButtonVisible: Boolean = false, + val mainButtonsVisible: Boolean = false, + val searchMode: Boolean = false, + val fullScreenMode: Boolean = false, ) { val actionButtonsContainerVisible: Boolean = submitButtonVisible || voiceInputButtonVisible || newLineButtonVisible } diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt index 24c5678a8f52..88f312515aec 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt @@ -41,6 +41,7 @@ import com.duckduckgo.browser.api.autocomplete.AutoCompleteSettings import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.common.utils.SingleLiveEvent import com.duckduckgo.common.utils.extensions.toBinaryString +import com.duckduckgo.duckchat.api.DuckAiFeatureState import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenConfigResolver import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command @@ -123,6 +124,7 @@ class InputScreenViewModel @AssistedInject constructor( private val voiceSearchAvailability: VoiceSearchAvailability, private val autoCompleteSettings: AutoCompleteSettings, private val duckChat: DuckChat, + private val duckAiFeatureState: DuckAiFeatureState, private val pixel: Pixel, private val sessionStore: InputScreenSessionStore, private val inputScreenDiscoveryFunnel: InputScreenDiscoveryFunnel, @@ -155,6 +157,7 @@ class InputScreenViewModel @AssistedInject constructor( newLineButtonVisible = false, mainButtonsVisible = false, searchMode = true, + fullScreenMode = duckAiFeatureState.showFullScreenMode.value, ), ) val visibilityState: StateFlow = _visibilityState.asStateFlow() @@ -414,14 +417,19 @@ class InputScreenViewModel @AssistedInject constructor( fun onChatSubmitted(query: String) { viewModelScope.launch { - val wasDuckAiOpenedBefore = duckChat.wasOpenedBefore() - if (isWebUrl(query)) { - command.value = Command.SubmitSearch(query) - } else { - command.value = Command.SubmitChat(query) - duckChat.openDuckChatWithAutoPrompt(query) + when { + _visibilityState.value.fullScreenMode -> { + val url = duckChat.getDuckChatUrl(query, true) + command.value = Command.SubmitSearch(url) + } + isWebUrl(query) -> command.value = Command.SubmitSearch(query) + else -> { + command.value = Command.SubmitChat(query) + duckChat.openDuckChatWithAutoPrompt(query) + } } + val wasDuckAiOpenedBefore = duckChat.wasOpenedBefore() val params = mapOf( DuckChatPixelParameters.WAS_USED_BEFORE to wasDuckAiOpenedBefore.toBinaryString(), From c91fe9bc2bc683b8e816f737ca0e6e641b9f6ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 16:14:58 +0100 Subject: [PATCH 5/8] add test for text submission --- .../ui/viewmodel/InputScreenViewModel.kt | 2 +- .../impl/messaging/fakes/FakeDuckChat.kt | 7 ++++++ .../inputscreen/InputScreenViewModelTest.kt | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt index 88f312515aec..c0bddf300508 100644 --- a/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt +++ b/duckchat/duckchat-impl/src/main/java/com/duckduckgo/duckchat/impl/inputscreen/ui/viewmodel/InputScreenViewModel.kt @@ -418,7 +418,7 @@ class InputScreenViewModel @AssistedInject constructor( fun onChatSubmitted(query: String) { viewModelScope.launch { when { - _visibilityState.value.fullScreenMode -> { + visibilityState.value.fullScreenMode -> { val url = duckChat.getDuckChatUrl(query, true) command.value = Command.SubmitSearch(url) } diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt index eeb02e0dbfed..ce7a189c204d 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/messaging/fakes/FakeDuckChat.kt @@ -46,6 +46,13 @@ class FakeDuckChat( openDuckChatWithPrefillCalls.add(query) } + override fun getDuckChatUrl( + query: String, + autoPrompt: Boolean, + ): String { + return "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5" + } + override fun isDuckChatUrl(uri: Uri): Boolean { return uri.toString().contains("duckchat") } diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/inputscreen/InputScreenViewModelTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/inputscreen/InputScreenViewModelTest.kt index 4e66a493b937..31b1fe1aca57 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/inputscreen/InputScreenViewModelTest.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/ui/inputscreen/InputScreenViewModelTest.kt @@ -20,6 +20,7 @@ import com.duckduckgo.browser.api.autocomplete.AutoCompleteFactory import com.duckduckgo.browser.api.autocomplete.AutoCompleteSettings import com.duckduckgo.common.test.CoroutineTestRule import com.duckduckgo.common.utils.extensions.toBinaryString +import com.duckduckgo.duckchat.api.DuckAiFeatureState import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckchat.impl.inputscreen.ui.InputScreenConfigResolver import com.duckduckgo.duckchat.impl.inputscreen.ui.command.Command @@ -42,6 +43,7 @@ import com.duckduckgo.history.api.NavigationHistory import com.duckduckgo.voice.api.VoiceSearchAvailability import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.advanceTimeBy @@ -86,6 +88,11 @@ class InputScreenViewModelTest { private val inputScreenConfigResolver: InputScreenConfigResolver = mock() private val omnibarRepository: OmnibarRepository = mock() + private val duckAiFeatureState: DuckAiFeatureState = mock() + private val fullScreenModeDisabledFlow = MutableStateFlow(false) + private val fullScreenModeEnabledFlow = MutableStateFlow(true) + private val duckChatURL = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5" + @Before fun setup() = runTest { @@ -95,9 +102,11 @@ class InputScreenViewModelTest { flowOf(AutoCompleteResult("", listOf(AutoCompleteDefaultSuggestion("suggestion")))), ) whenever(duckChat.wasOpenedBefore()).thenReturn(false) + whenever(duckChat.getDuckChatUrl(any(), any())).thenReturn(duckChatURL) whenever(inputScreenConfigResolver.useTopBar()).thenReturn(true) whenever(voiceSearchAvailability.isVoiceSearchAvailable).thenReturn(true) whenever(omnibarRepository.omnibarType).thenReturn(OmnibarType.SINGLE_TOP) + whenever(duckAiFeatureState.showFullScreenMode).thenReturn(fullScreenModeDisabledFlow) } private fun createViewModel(currentOmnibarText: String = ""): InputScreenViewModel = @@ -116,6 +125,7 @@ class InputScreenViewModelTest { inputScreenSessionUsageMetric = inputScreenSessionUsageMetric, inputScreenConfigResolver = inputScreenConfigResolver, omnibarRepository = omnibarRepository, + duckAiFeatureState = duckAiFeatureState, ) @Test @@ -2000,4 +2010,19 @@ class InputScreenViewModelTest { assertFalse(viewModel.visibilityState.value.mainButtonsVisible) } + + @Test + fun `when fullscreen mode enabled submitting chat sends a query to the main fragment`() = + runTest { + whenever(duckAiFeatureState.showFullScreenMode).thenReturn(fullScreenModeEnabledFlow) + whenever(inputScreenSessionStore.hasUsedSearchMode()).thenReturn(false) + whenever(inputScreenSessionStore.hasUsedChatMode()).thenReturn(false) + + val viewModel = createViewModel() + val query = "example" + + viewModel.onChatSubmitted(query) + + assertEquals(SubmitSearch(duckChatURL), viewModel.command.value) + } } From a857c875a16a84518eee398c0633ac85685285d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 22:58:17 +0100 Subject: [PATCH 6/8] add test for browser menu duck.ai --- .../app/browser/BrowserTabViewModelTest.kt | 22 +++++++++++++++++++ .../app/browser/BrowserTabViewModel.kt | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index 6fb01bcf6ab1..e49b021e7c03 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -459,6 +459,8 @@ class BrowserTabViewModelTest { private val mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow = MutableStateFlow(false) + private val mockDuckAiFeatureStateFullScreenModeFlow = MutableStateFlow(false) + private val mockExternalIntentProcessingState: ExternalIntentProcessingState = mock() private val mockVpnMenuStateProvider: VpnMenuStateProvider = mock() @@ -607,6 +609,7 @@ class BrowserTabViewModelTest { private val exampleUrl = "http://example.com" private val shortExampleUrl = "example.com" + private val duckChatURL = "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5" private val selectedTab = TabEntity("TAB_ID", exampleUrl, position = 0, sourceTabId = "TAB_ID_SOURCE") private val flowSelectedTab = MutableStateFlow(selectedTab) @@ -692,6 +695,7 @@ class BrowserTabViewModelTest { whenever(mockDuckAiFeatureState.showPopupMenuShortcut).thenReturn(MutableStateFlow(false)) whenever(mockDuckAiFeatureState.showInputScreen).thenReturn(mockDuckAiFeatureStateInputScreenFlow) whenever(mockDuckAiFeatureState.showInputScreenAutomaticallyOnNewTab).thenReturn(mockDuckAiFeatureStateInputScreenOpenAutomaticallyFlow) + whenever(mockDuckAiFeatureState.showFullScreenMode).thenReturn(mockDuckAiFeatureStateFullScreenModeFlow) whenever(mockExternalIntentProcessingState.hasPendingTabLaunch).thenReturn(mockHasPendingTabLaunchFlow) whenever(mockExternalIntentProcessingState.hasPendingDuckAiOpen).thenReturn(mockHasPendingDuckAiOpenFlow) whenever(mockVpnMenuStateProvider.getVpnMenuState()).thenReturn(flowOf(VpnMenuState.Hidden)) @@ -765,6 +769,8 @@ class BrowserTabViewModelTest { fakeContentScopeScriptsSubscriptionEventPluginPoint = FakeContentScopeScriptsSubscriptionEventPluginPoint() + whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckChatURL) + testee = BrowserTabViewModel( statisticsUpdater = mockStatisticsUpdater, @@ -6341,6 +6347,22 @@ class BrowserTabViewModelTest { verify(mockDuckChat).openDuckChat() } + @Test + fun whenDuckChatMenuItemClickedAndFullScreenModeThenDontOpenDuckChatScreen() = + runTest { + mockDuckAiFeatureStateFullScreenModeFlow.emit(true) + whenever(mockDuckChat.wasOpenedBefore()).thenReturn(false) + whenever(mockOmnibarConverter.convertQueryToUrl(duckChatURL, null)).thenReturn(duckChatURL) + + testee.onDuckChatMenuClicked() + + verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + + assertTrue(commandCaptor.lastValue is Navigate) + + verify(mockDuckChat, never()).openDuckChat() + } + @Test fun whenDuckChatMenuItemClickedAndItWasUsedBeforeThenOpenDuckChatAndSendPixel() = runTest { diff --git a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt index 8d6e8569c458..9f46a7d27606 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/BrowserTabViewModel.kt @@ -4476,7 +4476,6 @@ class BrowserTabViewModel @Inject constructor( if (duckAiFeatureState.showFullScreenMode.value) { val url = duckChat.getDuckChatUrl("", false) - pageRefreshed(url) onUserSubmittedQuery(url) } else { duckChat.openDuckChat() From 6580b4dbcbf1bae85fe11684d0b3676dfedf03ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Mon, 17 Nov 2025 23:37:18 +0100 Subject: [PATCH 7/8] cleanup test --- .../duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt index 036c11ea801f..5d52440c51ac 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt @@ -32,6 +32,7 @@ import com.duckduckgo.app.browser.SpecialUrlDetectorImpl.Companion.SMS_MAX_LENGT import com.duckduckgo.app.browser.applinks.ExternalAppIntentFlagsFeature import com.duckduckgo.app.browser.duckchat.AIChatQueryDetectionFeature import com.duckduckgo.app.pixels.remoteconfig.AndroidBrowserConfigFeature +import com.duckduckgo.duckchat.api.DuckAiFeatureState import com.duckduckgo.duckchat.api.DuckChat import com.duckduckgo.duckplayer.api.DuckPlayer import com.duckduckgo.feature.toggles.api.FakeFeatureToggleFactory @@ -43,6 +44,7 @@ import com.duckduckgo.privacy.config.api.TrackingParameters import com.duckduckgo.subscriptions.api.Subscriptions import junit.framework.TestCase.assertNull import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before @@ -73,12 +75,16 @@ class SpecialUrlDetectorImplTest { val mockDuckChat: DuckChat = mock() + val mockDuckAiFeature: DuckAiFeatureState = mock() + val mockAIChatQueryDetectionFeature: AIChatQueryDetectionFeature = mock() val mockAIChatQueryDetectionFeatureToggle: Toggle = mock() val androidBrowserConfigFeature: AndroidBrowserConfigFeature = FakeFeatureToggleFactory.create(AndroidBrowserConfigFeature::class.java) + private val mockDuckAiFullScreenMode = MutableStateFlow(false) + @Before fun setup() = runTest { testee = spy( @@ -92,12 +98,14 @@ class SpecialUrlDetectorImplTest { duckChat = mockDuckChat, aiChatQueryDetectionFeature = mockAIChatQueryDetectionFeature, androidBrowserConfigFeature = androidBrowserConfigFeature, + duckAiFeatureState = mockDuckAiFeature, ), ) whenever(mockPackageManager.queryIntentActivities(any(), anyInt())).thenReturn(emptyList()) whenever(mockDuckPlayer.willNavigateToDuckPlayer(any())).thenReturn(false) whenever(mockAIChatQueryDetectionFeatureToggle.isEnabled()).thenReturn(false) whenever(mockAIChatQueryDetectionFeature.self()).thenReturn(mockAIChatQueryDetectionFeatureToggle) + whenever(mockDuckAiFeature.showFullScreenMode).thenReturn(mockDuckAiFullScreenMode) androidBrowserConfigFeature.handleIntentScheme().setRawStoredState(State(true)) androidBrowserConfigFeature.validateIntentResolution().setRawStoredState(State(true)) } From 892ff1645a500bfa8379036f15e021249c18f801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Gonz=C3=A1lez?= Date: Wed, 19 Nov 2025 21:57:42 +0100 Subject: [PATCH 8/8] added missing tests --- .../app/browser/BrowserTabViewModelTest.kt | 17 +++++++++++ .../app/browser/SpecialUrlDetector.kt | 21 +++++++++++--- .../app/browser/SpecialUrlDetectorImplTest.kt | 9 ++++++ .../duckchat/impl/RealDuckChatTest.kt | 28 +++++++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt index e49b021e7c03..055245dd68f3 100644 --- a/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt +++ b/app/src/androidTest/java/com/duckduckgo/app/browser/BrowserTabViewModelTest.kt @@ -623,6 +623,7 @@ class BrowserTabViewModelTest { private val mockDeviceAppLookup: DeviceAppLookup = mock() private val mockDuckAiFullScreenMode = MutableStateFlow(false) + private val mockDuckAiFullScreenModeEnabled = MutableStateFlow(false) private lateinit var fakeContentScopeScriptsSubscriptionEventPluginPoint: FakeContentScopeScriptsSubscriptionEventPluginPoint private var fakeSettingsPageFeature = FakeFeatureToggleFactory.create(SettingsPageFeature::class.java) @@ -6422,6 +6423,22 @@ class BrowserTabViewModelTest { verify(mockDuckChat).openDuckChat() } + @Test + fun whenOnDuckChatOmnibarButtonClickedAndFullScreenModeThenOpenChatNotCalled() = runTest { + val duckAIUrl = "https://duckduckgo.com/?q=test" + mockDuckAiFeatureStateFullScreenModeFlow.emit(true) + whenever(mockDuckChat.getDuckChatUrl(any(), any())).thenReturn(duckAIUrl) + whenever(mockOmnibarConverter.convertQueryToUrl(duckAIUrl, null)).thenReturn(duckAIUrl) + + testee.onDuckChatOmnibarButtonClicked(query = "example", hasFocus = false, isNtp = true) + + verify(mockCommandObserver, atLeastOnce()).onChanged(commandCaptor.capture()) + val command = commandCaptor.lastValue as Navigate + assertEquals(duckAIUrl, command.url) + + verify(mockDuckChat, never()).openDuckChat() + } + @Test fun whenPageFinishedWithMaliciousSiteBlockedThenDoNotUpdateSite() { testee.browserViewState.value = diff --git a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt index bc07eebcc1b8..d778f0c81bc4 100644 --- a/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt +++ b/app/src/main/java/com/duckduckgo/app/browser/SpecialUrlDetector.kt @@ -55,7 +55,10 @@ class SpecialUrlDetectorImpl( private val androidBrowserConfigFeature: AndroidBrowserConfigFeature, ) : SpecialUrlDetector { - override fun determineType(initiatingUrl: String?, uri: Uri): UrlType { + override fun determineType( + initiatingUrl: String?, + uri: Uri, + ): UrlType { val uriString = uri.toString() return when (val scheme = uri.scheme) { @@ -71,12 +74,15 @@ class SpecialUrlDetectorImpl( null -> { if (subscriptions.shouldLaunchPrivacyProForUrl("https://$uriString")) { UrlType.ShouldLaunchPrivacyProLink - } else if (aiChatQueryDetectionFeature.self().isEnabled() && duckChat.isDuckChatUrl(uri)) { + } else if (aiChatQueryDetectionFeature.self() + .isEnabled() && duckChat.isDuckChatUrl(uri) && !duckAiFeatureState.showFullScreenMode.value + ) { UrlType.ShouldLaunchDuckChatLink } else { UrlType.SearchQuery(uriString) } } + else -> { val intentFlags = if (scheme == INTENT_SCHEME && androidBrowserConfigFeature.handleIntentScheme().isEnabled()) { URI_INTENT_SCHEME @@ -102,7 +108,10 @@ class SpecialUrlDetectorImpl( private fun buildSmsTo(uriString: String): UrlType = UrlType.Sms(uriString.removePrefix("$SMSTO_SCHEME:").truncate(SMS_MAX_LENGTH)) @Suppress("NewApi") // we use appBuildConfig - override fun processUrl(initiatingUrl: String?, uriString: String): UrlType { + override fun processUrl( + initiatingUrl: String?, + uriString: String, + ): UrlType { trackingParameters.cleanTrackingParameters(initiatingUrl = initiatingUrl, url = uriString)?.let { cleanedUrl -> return UrlType.TrackingParameterLink(cleanedUrl = cleanedUrl) } @@ -189,7 +198,11 @@ class SpecialUrlDetectorImpl( intentFlags: Int, userInitiated: Boolean, ): UrlType { - fun buildIntent(uriString: String, intentFlags: Int, userInitiated: Boolean): UrlType { + fun buildIntent( + uriString: String, + intentFlags: Int, + userInitiated: Boolean, + ): UrlType { return try { val intent = Intent.parseUri(uriString, intentFlags) // only proceed if something can handle it diff --git a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt index 5d52440c51ac..6b67ebd3334c 100644 --- a/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt +++ b/app/src/test/java/com/duckduckgo/app/browser/SpecialUrlDetectorImplTest.kt @@ -397,6 +397,15 @@ class SpecialUrlDetectorImplTest { assertTrue(result is SearchQuery) } + @Test + fun whenUrlIsDuckChatUrlAndFullscreenModeEnabledThenDuckChatTypeNotDetected() = runTest { + mockDuckAiFullScreenMode.emit(true) + whenever(mockAIChatQueryDetectionFeatureToggle.isEnabled()).thenReturn(true) + whenever(mockDuckChat.isDuckChatUrl(any())).thenReturn(true) + val result = testee.determineType("duckduckgo.com") + assertTrue(result is SearchQuery) + } + @Test fun whenUrlIsParametrizedQueryThenSearchQueryTypeDetected() { val type = testee.determineType("foo site:duckduckgo.com") as SearchQuery diff --git a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt index bd1ab54f0295..0f28cbc90520 100644 --- a/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt +++ b/duckchat/duckchat-impl/src/test/kotlin/com/duckduckgo/duckchat/impl/RealDuckChatTest.kt @@ -1090,6 +1090,34 @@ class RealDuckChatTest { assertTrue(testee.showInputScreenOnSystemSearchLaunch.value) } + @Test + fun `when get duck chat url with query and autoprompt then return correct url`() = runTest { + val url = testee.getDuckChatUrl("query", true) + + assertTrue(url == "https://duckduckgo.com/?q=query&prompt=1&ia=chat&duckai=5") + } + + @Test + fun `when get duck chat url with query and no autoprompt then return correct url`() = runTest { + val url = testee.getDuckChatUrl("query", false) + + assertTrue(url == "https://duckduckgo.com/?q=query&ia=chat&duckai=5") + } + + @Test + fun `when get duck chat url with empty query and no autoprompt then return correct url`() = runTest { + val url = testee.getDuckChatUrl("", false) + + assertTrue(url == "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5") + } + + @Test + fun `when get duck chat url with empty query and autoprompt then return correct url`() = runTest { + val url = testee.getDuckChatUrl("", true) + + assertTrue(url == "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=5") + } + companion object { val SETTINGS_JSON = """ {