From 4e4a2435a777c21bf7d4baba47dad2a0acc5ff1e Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:26:14 +0200 Subject: [PATCH 1/2] Add support for custom view in FrontendScreen Instanciate the HAWebChromeClient from Compose to get the custom view --- .../android/frontend/FrontendScreen.kt | 34 +++++++- .../android/frontend/FrontendViewModel.kt | 61 +++++++++----- .../android/util/HAWebChromeClient.kt | 15 ++++ .../frontend/FrontendScreenScreenshotTest.kt | 34 ++++++++ ...ustom view overlay_foldable_c908f502_0.png | Bin 0 -> 25358 bytes ...h custom view overlay_phone_e05166be_0.png | Bin 0 -> 15691 bytes ...iew overlay_phone_landscape_9e00b29d_0.png | Bin 0 -> 12860 bytes ...om view overlay_small_phone_66e7bbf2_0.png | Bin 0 -> 10565 bytes ... custom view overlay_tablet_2f22c4ea_0.png | Bin 0 -> 21655 bytes ...ew overlay_tablet_landscape_62cae397_0.png | Bin 0 -> 19786 bytes .../android/frontend/FrontendScreenTest.kt | 45 +++++++++++ .../android/frontend/FrontendViewModelTest.kt | 75 +++++++++++++++++- .../android/util/HAWebChromeClientTest.kt | 31 ++++++++ 13 files changed, 273 insertions(+), 22 deletions(-) create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_foldable_c908f502_0.png create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_e05166be_0.png create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_landscape_9e00b29d_0.png create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_small_phone_66e7bbf2_0.png create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_2f22c4ea_0.png create mode 100644 app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_landscape_62cae397_0.png diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt index 17b1951189c..a9907272681 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendScreen.kt @@ -8,6 +8,7 @@ import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient +import androidx.annotation.VisibleForTesting import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -38,8 +39,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.viewinterop.AndroidView import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.homeassistant.companion.android.common.R as commonR @@ -79,6 +82,10 @@ import timber.log.Timber /** Minimum swipe velocity (pixels/second) to trigger a gesture action. */ private const val MINIMUM_GESTURE_VELOCITY = 75f +/** Test tag applied to the WebView custom view fullscreen overlay. */ +@VisibleForTesting +internal const val CUSTOM_VIEW_OVERLAY_TAG = "custom_view_overlay" + /** * Frontend screen that renders based on the ViewModel's current view state. * @@ -114,6 +121,16 @@ internal fun FrontendScreen( val pendingDialog by viewModel.pendingDialog.collectAsStateWithLifecycle() val pendingFileChooser by viewModel.pendingFileChooser.collectAsStateWithLifecycle() + // The fullscreen View handed over by the WebView is Activity-scoped. Keep it in screen + // state so it does not leak across configuration changes via the ViewModel. + var customView by remember { mutableStateOf(null) } + val webChromeClient = remember(viewModel) { + viewModel.createWebChromeClient( + onShowCustomView = { customView = it }, + onHideCustomView = { customView = null }, + ) + } + // Create SecurityLevel ViewModel only when needed val securityLevelViewModel: LocationForSecureConnectionViewModel? = if (viewState is FrontendViewState.SecurityLevelRequired) { @@ -129,7 +146,8 @@ internal fun FrontendScreen( viewState = viewState, errorStateProvider = viewModel as FrontendConnectionErrorStateProvider, webViewClient = viewModel.webViewClient, - webChromeClient = viewModel.webChromeClient, + webChromeClient = webChromeClient, + customView = customView, frontendJsCallback = viewModel.frontendJsCallback, pendingPermissionRequest = pendingPermissionRequest, pendingDialog = pendingDialog, @@ -172,6 +190,7 @@ internal fun FrontendScreenContent( onShowSnackbar: suspend (message: String, action: String?) -> Boolean, onWebViewCreationFailed: (Throwable) -> Unit, modifier: Modifier = Modifier, + customView: View? = null, pendingPermissionRequest: PermissionRequest? = null, pendingDialog: FrontendDialog? = null, pendingFileChooser: FileChooserRequest? = null, @@ -222,6 +241,8 @@ internal fun FrontendScreenContent( onFullscreenChanged = onExoPlayerFullscreenChanged, ) + CustomViewOverlay(customView = customView) + StateOverlay( viewState = viewState, errorStateProvider = errorStateProvider, @@ -596,6 +617,17 @@ private fun WebViewEffects( } } +@Composable +private fun CustomViewOverlay(customView: View?) { + val view: View = customView ?: return + AndroidView( + factory = { view }, + modifier = Modifier + .fillMaxSize() + .testTag(CUSTOM_VIEW_OVERLAY_TAG), + ) +} + @Composable private fun ExoPlayerOverlay(contentState: FrontendViewState.Content?, onFullscreenChanged: (Boolean) -> Unit) { val exoState = contentState?.exoPlayerState ?: return diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt index ee5a78f117a..7dd6496d7cd 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModel.kt @@ -1,5 +1,6 @@ package io.homeassistant.companion.android.frontend +import android.view.View import androidx.annotation.VisibleForTesting import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -218,26 +219,6 @@ internal class FrontendViewModel @VisibleForTesting constructor( /** The current pending file chooser request from the WebView, or null if none. */ val pendingFileChooser: StateFlow = fileChooserManager.pendingFileChooser - val webChromeClient: HAWebChromeClient = HAWebChromeClient( - onPermissionRequest = { request -> - viewModelScope.launch { - permissionManager.onWebViewPermissionRequest(request) - } - }, - onJsConfirm = { message, jsResult -> - viewModelScope.launch { - if (dialogManager.showJsConfirm(message)) jsResult.confirm() else jsResult.cancel() - } - true - }, - onShowFileChooser = { filePathCallback, fileChooserParams -> - viewModelScope.launch { - filePathCallback.onReceiveValue(fileChooserManager.pickFiles(fileChooserParams)) - } - true - }, - ) - /** The current pending permission request that needs user approval, or null if none. */ val pendingPermissionRequest = permissionManager.pendingPermissionRequest @@ -306,6 +287,46 @@ internal class FrontendViewModel @VisibleForTesting constructor( } } + /** + * Builds an [HAWebChromeClient] wired to this ViewModel for permission/JS handling, while + * delegating WebView fullscreen view ownership to the caller. + * + * The fullscreen [android.view.View] handed over by `onShowCustomView` is bound to the + * WebView's Activity context. Holding it in ViewModel state would leak that Activity across + * configuration changes, so the caller (a Composable) keeps the View in screen-scoped state + * and supplies setters via [onShowCustomView] and [onHideCustomView]. The ViewModel still + * owns the system-fullscreen request and emits [FrontendEvent.RequestFullscreen] on the + * caller's behalf. + */ + fun createWebChromeClient(onShowCustomView: (View) -> Unit, onHideCustomView: () -> Unit): HAWebChromeClient = + HAWebChromeClient( + onPermissionRequest = { request -> + viewModelScope.launch { + permissionManager.onWebViewPermissionRequest(request) + } + }, + onJsConfirm = { message, jsResult -> + viewModelScope.launch { + if (dialogManager.showJsConfirm(message)) jsResult.confirm() else jsResult.cancel() + } + true + }, + onShowFileChooser = { filePathCallback, fileChooserParams -> + viewModelScope.launch { + filePathCallback.onReceiveValue(fileChooserManager.pickFiles(fileChooserParams)) + } + true + }, + onShowCustomView = { view -> + onShowCustomView(view) + _events.tryEmit(FrontendEvent.RequestFullscreen(fullscreen = true)) + }, + onHideCustomView = { + onHideCustomView() + _events.tryEmit(FrontendEvent.RequestFullscreen(fullscreen = false)) + }, + ) + fun onRetry() { _viewState.update { FrontendViewState.LoadServer(serverId = it.serverId) diff --git a/app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebChromeClient.kt b/app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebChromeClient.kt index 7e4b29f01eb..e0fb9e356ce 100644 --- a/app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebChromeClient.kt +++ b/app/src/main/kotlin/io/homeassistant/companion/android/util/HAWebChromeClient.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.util import android.net.Uri +import android.view.View import android.webkit.JsResult import android.webkit.PermissionRequest import android.webkit.ValueCallback @@ -15,6 +16,9 @@ import android.webkit.WebView * Return `true` to indicate the dialog is handled. * @param onShowFileChooser Callback when the page requests a file upload. * Return `true` to indicate the file chooser is handled. + * @param onShowCustomView Callback when the page enters fullscreen (e.g. HTML5 video). + * Receives the fullscreen [View] handed over by the WebView. + * @param onHideCustomView Callback when the page leaves fullscreen. */ class HAWebChromeClient( private val onPermissionRequest: (PermissionRequest) -> Unit = {}, @@ -23,6 +27,8 @@ class HAWebChromeClient( filePathCallback: ValueCallback>, fileChooserParams: FileChooserParams, ) -> Boolean = { _, _ -> false }, + private val onShowCustomView: (View) -> Unit = {}, + private val onHideCustomView: () -> Unit = {}, ) : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest?) { @@ -46,4 +52,13 @@ class HAWebChromeClient( } return super.onShowFileChooser(webView, filePathCallback, fileChooserParams) } + + override fun onShowCustomView(view: View?, callback: CustomViewCallback?) { + view?.let { onShowCustomView.invoke(it) } + } + + override fun onHideCustomView() { + onHideCustomView.invoke() + super.onHideCustomView() + } } diff --git a/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest.kt b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest.kt index 13d96fc5101..c503e8be788 100644 --- a/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest.kt +++ b/app/src/screenshotTest/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest.kt @@ -1,9 +1,12 @@ package io.homeassistant.companion.android.frontend import android.annotation.SuppressLint +import android.graphics.Color as AndroidColor +import android.view.View import android.webkit.WebChromeClient import android.webkit.WebViewClient import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import com.android.tools.screenshot.PreviewTest import io.homeassistant.companion.android.common.R as commonR import io.homeassistant.companion.android.common.compose.theme.HAThemeForPreview @@ -315,4 +318,35 @@ class FrontendScreenScreenshotTest { ) } } + + @PreviewTest + @HAPreviews + @Composable + fun `FrontendScreen Content state with custom view overlay`() { + HAThemeForPreview { + val context = LocalContext.current + val view = View(context).apply { setBackgroundColor(AndroidColor.RED) } + FrontendScreenContent( + onBackClick = {}, + viewState = FrontendViewState.Content( + serverId = 1, + url = "https://example.com", + ), + customView = view, + webViewClient = WebViewClient(), + webChromeClient = WebChromeClient(), + frontendJsCallback = FrontendJsBridge.noOp, + onBlockInsecureRetry = {}, + onOpenExternalLink = {}, + onBlockInsecureHelpClick = {}, + onOpenSettings = {}, + onChangeSecurityLevel = {}, + onOpenLocationSettings = {}, + onConfigureHomeNetwork = { _ -> }, + onSecurityLevelHelpClick = {}, + onShowSnackbar = { _, _ -> false }, + onWebViewCreationFailed = {}, + ) + } + } } diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_foldable_c908f502_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_foldable_c908f502_0.png new file mode 100644 index 0000000000000000000000000000000000000000..5132216967a47095b28e82736fa2a5d032fea7f9 GIT binary patch literal 25358 zcmeI4!7GDt7{?#1HDhab5JFqIDqLu2*u<12xzduuZgNn{Wp=MfhnE1W`NS5F-LxMy&utR0W`F zMBpu>R^YX3L?A4v6$lHe0%5_3Kv+;K5EfJg!h#Wju%K2TEEo|83u*8PW@{7GqiKkY2XPO_8LaldGFAaJwfj10Df zouR@E52J?QjT}1#6bL8)1p*2L6o3K&1)xAcff=))T%tfA6@UUzV7g48!1XBD{xu({ z%|X35({Xnrm$^hN>hiM&EQ&?3C_^$PO@IPW017|>D4@1d1*ol;WL1)zXbpuvdIEaovt1r!LNKxs%t dPl51Hy@Esej2%pEc=s>Vc&YJp@@OoR{{rWw;1mD= literal 0 HcmV?d00001 diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_e05166be_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_e05166be_0.png new file mode 100644 index 0000000000000000000000000000000000000000..cc28507ef5f13a360cfc64aa526ea36290f878b4 GIT binary patch literal 15691 zcmeI2u}Z^G6o!wrT2qLM4oVS0D!8_|2-=|~MFY}^I5>rhFW@8r2TPrNf^O>QqIB<4 zK}39li~;FnyLNC8|M&*FxO~eE_mX7zx%Zs!KlfZ8Gj^2!WrR=a7H*IoDt3lXM{7t8R3k6I#oA6&1 z_;3`L9Yx~)-Pc3sG&ymmsQ8ncx$gl@xb35a$6YTKOqv6PNAyNW1X@EV{N--h>cb@DFi`8M7v;P zp;#ygg0&#T#vfoIXcdrnN7&ugN)XOgmt}T~!fy7h@ zgn~jK6chrXpb!WJ1A$Ob2!w({AQa4eV5guE2nB^eC@2I%K_L(d1_Gg=5PlO1`Dkm>whn g87WAB1V|wNT7q%su;f0ES`o7+ESD;FbG};t29jn`TmS$7 literal 0 HcmV?d00001 diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_small_phone_66e7bbf2_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_small_phone_66e7bbf2_0.png new file mode 100644 index 0000000000000000000000000000000000000000..e91708aacc5a62a6eca890e0fbb464f7ed51cbb9 GIT binary patch literal 10565 zcmeI0Axi{N6ot?14mvI~AXvnt2E{hEt*)!?fGmrcWx!-JZX85VyFoDMA5bg?!7!RO z7zC3+#Aet9F{?Q5z1d%2uzW9FE{_-PJLlYU?@_0{7S*O|lA@&5+>k~y$*%_^MdtnK zZdqCgljhQ9cdP%r7%2|(tIzqD@9oFl!C9x*f2b7qinIIr<+&(34bw6Ts*^67zL$Bg zIEQgo-kaqb1py(J6>!0p0dcTpKrL++h8dXTyx z8}mXJ@t8!-B%mQdC!rdJ&?1hap-A*i344G&Fh&u3fITocV5qg~?;NBid3d`GgY{+CmeDT$*1h6(;64m)Z{_Ddp6xf3Bu0nVaj{(ZER~Yd@2s-P4#ao;MPqFKR21Up2}F z*9iK79IOMl!OH+Q;0D~lFkl!!27sb>0mFb{z%XDKFbp^)5t%p_a7gsCfXskI@ekdw z_|e{TS#N&!`|e{cbJ)?`eJbaRi>f-oZYAD%3-#f=zY%3)fiK!&pI?`YkzX|y#C^dd zL@y+CA>k1*FC=EC#6BYSR0%i$4vZ3k1K_}902}}Z?#u`r00$-m-~c!<82|^sfytoD z;h?#ny|sU-@b#%RRNd}(H4!T2HyIEccDwm8yXydEw;o`2%x*H^8v!wZ$$)PJ!~iA( zP6EJzlL2r59GDD%1K_}902}}ZZdV{C{C#o|wHhmF*UX8pg~!YFTJZZvf-J6|APdN1 zG5}dX7Lx(U0vA+ZllZRXVa~K_gtr~e5x^>_PP_Ybzj0$ z(x)%%dLXX*Qj5B#A77WCzHqI6OZO!>BIX^|gM`hEEU#dqM6f6kxDdgC2%gqk99S=e zh1IhIidaG!Bt8>&ySgp z#=g2I&3aeLZMM&9|ENBq1Fq>$rp09V7{DxH8t~vJ>fr$%-~k?tTtGUYmr#`!Jm?u9 zejv&sv~)EN0uS&25Ae`Csdlw-pH{A7@B5~Fyn8q?mh>y+D*3K5y78`ByMv&u#kNhcE8#Ewv6Y12Y7%7crXru-EVfk+5L9mLC*k3N+_YHgq{+5O6avm }, + onSecurityLevelHelpClick = {}, + onShowSnackbar = { _, _ -> true }, + onWebViewCreationFailed = {}, + ) + } + + composeTestRule.onNodeWithTag(CUSTOM_VIEW_OVERLAY_TAG).assertIsDisplayed() + } + + @Test + fun `Given Content state without customView then overlay is not displayed`() { + composeTestRule.apply { + setFrontendScreen( + viewState = FrontendViewState.Content( + serverId = 1, + url = "https://example.com", + ), + ) + onNodeWithTag(CUSTOM_VIEW_OVERLAY_TAG).assertDoesNotExist() + } + } + private fun AndroidComposeTestRule, HiltComponentActivity>.assertIsLoading(show: Boolean) { val node = onNodeWithContentDescription(stringResource(commonR.string.loading_content_description)) if (show) { diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt index 6dc958f8f5d..ae196fc44f5 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.frontend import android.net.Uri +import android.view.View import android.webkit.HttpAuthHandler import android.webkit.JsResult import android.webkit.ValueCallback @@ -66,6 +67,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertSame import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -1106,7 +1108,7 @@ class FrontendViewModelTest { private fun captureJsConfirmCallback(): Pair Boolean> { val viewModel = createViewModel() - val client = viewModel.webChromeClient + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) val callback: (String, JsResult) -> Boolean = { message, result -> // view and url are unused by HAWebChromeClient when message and result are non-null client.onJsConfirm(null, null, message, result) @@ -1626,4 +1628,75 @@ class FrontendViewModelTest { verify { exoPlayerManager.close() } } } + + @Nested + inner class CustomView { + + @Test + fun `Given factory client when onShowCustomView then provided show callback receives the View`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + val viewModel = createViewModel() + var capturedView: View? = null + val client = viewModel.createWebChromeClient( + onShowCustomView = { capturedView = it }, + onHideCustomView = {}, + ) + val customView = mockk(relaxed = true) + + client.onShowCustomView(customView, mockk(relaxed = true)) + + assertSame(customView, capturedView) + } + + @Test + fun `Given factory client when onHideCustomView then provided hide callback is invoked`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + val viewModel = createViewModel() + var hideInvoked = false + val client = viewModel.createWebChromeClient( + onShowCustomView = {}, + onHideCustomView = { hideInvoked = true }, + ) + + client.onHideCustomView() + + assertTrue(hideInvoked) + } + + @Test + fun `Given factory client when onShowCustomView then RequestFullscreen true emitted`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + val viewModel = createViewModel() + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) + + viewModel.events.test { + client.onShowCustomView(mockk(relaxed = true), mockk(relaxed = true)) + assertEquals(FrontendEvent.RequestFullscreen(fullscreen = true), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `Given factory client when onHideCustomView then RequestFullscreen false emitted`() = runTest { + every { urlManager.serverUrlFlow(any(), any()) } returns flowOf( + UrlLoadResult.Success(url = testUrlWithAuth, serverId = serverId), + ) + val viewModel = createViewModel() + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) + + viewModel.events.test { + client.onShowCustomView(mockk(relaxed = true), mockk(relaxed = true)) + assertEquals(FrontendEvent.RequestFullscreen(fullscreen = true), awaitItem()) + client.onHideCustomView() + assertEquals(FrontendEvent.RequestFullscreen(fullscreen = false), awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + } } diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebChromeClientTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebChromeClientTest.kt index bb7d944873c..02220b02833 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebChromeClientTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/util/HAWebChromeClientTest.kt @@ -146,4 +146,35 @@ class HAWebChromeClientTest { assertFalse(handled) verify(exactly = 0) { filePathCallback.onReceiveValue(any()) } } + + @Test + fun `Given onShowCustomView callback when triggered then view is forwarded`() { + var capturedView: android.view.View? = null + val client = HAWebChromeClient(onShowCustomView = { view -> capturedView = view }) + val view = mockk(relaxed = true) + + client.onShowCustomView(view, mockk(relaxed = true)) + + assertTrue(capturedView === view) + } + + @Test + fun `Given onShowCustomView with null view then callback is not invoked`() { + var invoked = false + val client = HAWebChromeClient(onShowCustomView = { invoked = true }) + + client.onShowCustomView(null, mockk(relaxed = true)) + + assertFalse(invoked) + } + + @Test + fun `Given onHideCustomView callback when triggered then callback is invoked`() { + var invoked = false + val client = HAWebChromeClient(onHideCustomView = { invoked = true }) + + client.onHideCustomView() + + assertTrue(invoked) + } } From a4fa47ffc1c68759db270eaa979daaa0aaea9688 Mon Sep 17 00:00:00 2001 From: Timothy <6560631+TimoPtr@users.noreply.github.com> Date: Tue, 5 May 2026 14:41:47 +0200 Subject: [PATCH 2/2] Adjust tests after rebase --- .../android/frontend/FrontendViewModelTest.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt index ae196fc44f5..b33f4ada2b6 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendViewModelTest.kt @@ -1214,7 +1214,9 @@ class FrontendViewModelTest { val filePathCallback = mockk>>(relaxed = true) val fileChooserParams = mockk(relaxed = true) - val handled = viewModel.webChromeClient.onShowFileChooser( + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) + + val handled = client.onShowFileChooser( mockk(relaxed = true), filePathCallback, fileChooserParams, @@ -1236,7 +1238,9 @@ class FrontendViewModelTest { val viewModel = createViewModel() val filePathCallback = mockk>>(relaxed = true) - viewModel.webChromeClient.onShowFileChooser( + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) + + val handled = client.onShowFileChooser( mockk(relaxed = true), filePathCallback, mockk(relaxed = true), @@ -1263,7 +1267,9 @@ class FrontendViewModelTest { val viewModel = createViewModel() val filePathCallback = mockk>>(relaxed = true) - viewModel.webChromeClient.onShowFileChooser( + val client = viewModel.createWebChromeClient(onShowCustomView = {}, onHideCustomView = {}) + + val handled = client.onShowFileChooser( mockk(relaxed = true), filePathCallback, mockk(relaxed = true),