package com.contentsquare.rn.csq import android.app.Activity import android.os.Build import android.webkit.WebBackForwardList import android.webkit.WebHistoryItem import android.webkit.WebView import android.webkit.WebViewClient import androidx.test.ext.junit.runners.AndroidJUnit4 import com.contentsquare.android.R import com.contentsquare.rn.utils.ReactNativeUiThreadUtil import com.contentsquare.rn.utils.ReactNativeViewFinder import com.contentsquare.rn.utils.VersionUtils import com.contentsquare.rn.utils.WebViewUtils import com.contentsquare.rn.webview.BridgeWebViewClient import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.runs import io.mockk.slot import io.mockk.unmockkAll import io.mockk.verify import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(manifest = Config.NONE) class CSQWebViewTest { // mocks private val mockActivity = mockk() private val mockReactApplicationContext = mockk() private val mockReactNativeUiThreadUtil = mockk() private val mockReactNativeViewFinder = mockk() private val mockWebView = mockk(relaxed = true) private val mockWebViewClient = mockk(relaxed = true) private val mockRCTDeviceEventEmitter = mockk() private val mockWebBackForwardList = mockk() private val mockWebHistoryItem = mockk() private val mockWebViewTag = 999 // non-mocks private lateinit var underTest: CSQWebView @Before fun init() { every { mockReactApplicationContext.currentActivity } returns mockActivity every { mockReactNativeUiThreadUtil.runOnUiThread(any()) } answers { firstArg().run() } every { mockReactNativeViewFinder.findWebView( mockReactApplicationContext, mockWebViewTag, any() ) } answers { val callback = thirdArg() callback.onWebViewFound(mockWebView) } every { mockReactApplicationContext.getJSModule(RCTDeviceEventEmitter::class.java) } returns mockRCTDeviceEventEmitter every { mockRCTDeviceEventEmitter.emit(any(), any()) } just runs mockkObject(WebViewUtils) mockkObject(VersionUtils) every { WebViewUtils.getWebViewClient(any()) } returns mockWebViewClient every { mockWebView.setTag(any(), any()) } just runs // Initialize the class under test after setting up mocks underTest = CSQWebView( mockReactApplicationContext, mockReactNativeUiThreadUtil, mockReactNativeViewFinder ) } @After fun tearDown() { unmockkAll() } @Test fun `Given no current activity, When register webView, Then do not proceed with registration`() { // given every { mockReactApplicationContext.currentActivity } returns null // when underTest.registerWebView(mockWebViewTag) // then verify(exactly = 0) { mockReactNativeUiThreadUtil.runOnUiThread(any()) } } @Test fun `Given webView found, When register webView, Then prepareWebView sets activity tag correctly`() { // when underTest.registerWebView(mockWebViewTag) // then verify(exactly = 1) { mockWebView.setTag( R.string.contentsquare_react_native_web_view_activity_tag, mockActivity ) } verify(exactly = 1) { mockRCTDeviceEventEmitter.emit("onCSQWebViewInjected", null) } } @Test fun `Given webView found, When unregister webView, Then prepareWebView sets activity tag correctly`() { // when underTest.unregisterWebView(mockWebViewTag) // then verify(exactly = 1) { mockWebView.setTag( R.string.contentsquare_react_native_web_view_activity_tag, mockActivity ) } } @Test fun `Given API level 26 or higher and valid webViewClient, When register webView, Then handleBlankPageRemoval sets new webViewClient`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns true // when underTest.registerWebView(mockWebViewTag) // then val webViewClientSlot = slot() verify(exactly = 1) { mockWebView.webViewClient = capture(webViewClientSlot) } // Verify the captured client is a BridgeWebViewClient val capturedClient = webViewClientSlot.captured assert(capturedClient is BridgeWebViewClient) } @Test fun `Given API level below 26, When register webView, Then handleBlankPageRemoval does not set webViewClient`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns false // when underTest.registerWebView(mockWebViewTag) // then verify(exactly = 0) { mockWebView.webViewClient = any() } } @Test fun `Given null webViewClient, When register webView, Then handleBlankPageRemoval does not set webViewClient`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns true every { WebViewUtils.getWebViewClient(any()) } returns null // when underTest.registerWebView(mockWebViewTag) // then verify(exactly = 0) { mockWebView.webViewClient = any() } } @Test fun `Given blank page in history, When onPageStarted called, Then webView history is cleared`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns true every { mockWebView.copyBackForwardList() } returns mockWebBackForwardList every { mockWebBackForwardList.size } returns 2 every { mockWebBackForwardList.getItemAtIndex(0) } returns mockWebHistoryItem every { mockWebHistoryItem.originalUrl } returns "about:blank" every { mockWebView.clearHistory() } just runs // when underTest.registerWebView(mockWebViewTag) // then val webViewClientSlot = slot() verify { mockWebView.webViewClient = capture(webViewClientSlot) } // Simulate onPageStarted call val capturedClient = webViewClientSlot.captured capturedClient.onPageStarted(mockWebView, "https://example.com", null) // then verify(exactly = 1) { mockWebView.clearHistory() } } @Test fun `Given no blank page in history, When onPageStarted called, Then webView history is not cleared`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns true every { mockWebView.copyBackForwardList() } returns mockWebBackForwardList every { mockWebBackForwardList.size } returns 2 every { mockWebBackForwardList.getItemAtIndex(0) } returns mockWebHistoryItem every { mockWebHistoryItem.originalUrl } returns "https://example.com" // when underTest.registerWebView(mockWebViewTag) // then val webViewClientSlot = slot() verify { mockWebView.webViewClient = capture(webViewClientSlot) } // Simulate onPageStarted call val capturedClient = webViewClientSlot.captured capturedClient.onPageStarted(mockWebView, "https://example.com", null) // then verify(exactly = 0) { mockWebView.clearHistory() } } @Test fun `Given history size not equal to 2, When onPageStarted called, Then webView history is not cleared`() { // given every { VersionUtils.isApiLevelAtLeast(Build.VERSION_CODES.O) } returns true every { mockWebView.copyBackForwardList() } returns mockWebBackForwardList every { mockWebBackForwardList.size } returns 1 // when underTest.registerWebView(mockWebViewTag) // then val webViewClientSlot = slot() verify { mockWebView.webViewClient = capture(webViewClientSlot) } // Simulate onPageStarted call val capturedClient = webViewClientSlot.captured capturedClient.onPageStarted(mockWebView, "https://example.com", null) // then verify(exactly = 0) { mockWebView.clearHistory() } } @Test fun `Given different webView tags, When register different webViews, Then each should be processed separately`() { // given val webViewTag1 = 100 val webViewTag2 = 200 val mockWebView1 = mockk(relaxed = true) val mockWebView2 = mockk(relaxed = true) every { mockReactNativeViewFinder.findWebView( mockReactApplicationContext, webViewTag1, any() ) } answers { val callback = thirdArg() callback.onWebViewFound(mockWebView1) } every { mockReactNativeViewFinder.findWebView( mockReactApplicationContext, webViewTag2, any() ) } answers { val callback = thirdArg() callback.onWebViewFound(mockWebView2) } every { mockWebView1.setTag(any(), any()) } just runs every { mockWebView2.setTag(any(), any()) } just runs // when underTest.registerWebView(webViewTag1) underTest.registerWebView(webViewTag2) // then verify(exactly = 1) { mockWebView1.setTag( R.string.contentsquare_react_native_web_view_activity_tag, mockActivity ) } verify(exactly = 1) { mockWebView2.setTag( R.string.contentsquare_react_native_web_view_activity_tag, mockActivity ) } verify(exactly = 2) { mockRCTDeviceEventEmitter.emit("onCSQWebViewInjected", null) } } @Test fun `Given multiple register calls, When register same webView multiple times, Then each call processes independently`() { // when underTest.registerWebView(mockWebViewTag) underTest.registerWebView(mockWebViewTag) // then verify(exactly = 2) { mockWebView.setTag( R.string.contentsquare_react_native_web_view_activity_tag, mockActivity ) } verify(exactly = 2) { mockRCTDeviceEventEmitter.emit("onCSQWebViewInjected", null) } } @Test fun `Given webView not found, When findWebView does not call callback, Then no processing occurs`() { // given every { mockReactNativeViewFinder.findWebView( mockReactApplicationContext, mockWebViewTag, any() ) } just runs // Don't call the callback at all // when underTest.registerWebView(mockWebViewTag) // then verify(exactly = 0) { mockRCTDeviceEventEmitter.emit("onCSQWebViewInjected", null) } verify(exactly = 0) { mockWebView.setTag(any(), any()) } } }