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 00000000000..5132216967a Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_foldable_c908f502_0.png differ 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 00000000000..cc28507ef5f Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_e05166be_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_landscape_9e00b29d_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_landscape_9e00b29d_0.png new file mode 100644 index 00000000000..c3c0f43bae9 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_phone_landscape_9e00b29d_0.png differ 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 00000000000..e91708aacc5 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_small_phone_66e7bbf2_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_2f22c4ea_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_2f22c4ea_0.png new file mode 100644 index 00000000000..83b54be269f Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_2f22c4ea_0.png differ diff --git a/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_landscape_62cae397_0.png b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_landscape_62cae397_0.png new file mode 100644 index 00000000000..d2fddd880e9 Binary files /dev/null and b/app/src/screenshotTestFullDebug/reference/io/homeassistant/companion/android/frontend/FrontendScreenScreenshotTest/FrontendScreen Content state with custom view overlay_tablet_landscape_62cae397_0.png differ diff --git a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenTest.kt b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenTest.kt index b5e43bbf532..2946d4d19f7 100644 --- a/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenTest.kt +++ b/app/src/test/kotlin/io/homeassistant/companion/android/frontend/FrontendScreenTest.kt @@ -1,6 +1,7 @@ package io.homeassistant.companion.android.frontend import android.Manifest +import android.view.View import android.webkit.PermissionRequest as WebViewPermissionRequest import android.webkit.WebChromeClient import android.webkit.WebViewClient @@ -9,6 +10,7 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.ActivityResultRegistryOwner import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule @@ -482,6 +484,49 @@ class FrontendScreenTest { } } + @Test + fun `Given Content state with customView then overlay is displayed`() { + composeTestRule.setContent { + val context = LocalContext.current + FrontendScreenContent( + onBackClick = {}, + viewState = FrontendViewState.Content( + serverId = 1, + url = "https://example.com", + ), + customView = View(context), + webViewClient = WebViewClient(), + webChromeClient = WebChromeClient(), + frontendJsCallback = FrontendJsBridge.noOp, + onBlockInsecureRetry = {}, + onOpenExternalLink = {}, + onBlockInsecureHelpClick = {}, + onOpenSettings = {}, + onChangeSecurityLevel = {}, + onOpenLocationSettings = {}, + onConfigureHomeNetwork = { _ -> }, + 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..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 @@ -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) @@ -1212,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, @@ -1234,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), @@ -1261,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), @@ -1626,4 +1634,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) + } }