Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion capture-sdk/sdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ dependencies {
androidTestImplementation(libs.androidx.test.uiautomator)
androidTestImplementation(libs.mockito.android)
androidTestImplementation(libs.androidx.multidex)

testImplementation(libs.mockito.kotlin2)
androidTestImplementation(libs.mockito.kotlin2)
androidTestUtil(libs.androidx.test.orchestrator)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.gini.android.capture;

import androidx.annotation.Nullable;

/**
* Helper class to set the {@link GiniCapture} instance for instrumentation tests.
*/
public class GiniCaptureHelperForInstrumentationTests {
public static void setGiniCaptureInstance(@Nullable final GiniCapture giniCapture) {
GiniCapture.setInstance(giniCapture);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package net.gini.android.capture.ginicapture

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import androidx.fragment.app.testing.FragmentScenario
import androidx.lifecycle.Lifecycle
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import jersey.repackaged.jsr166e.CompletableFuture
import net.gini.android.capture.DocumentImportEnabledFileTypes
import net.gini.android.capture.EntryPoint
import net.gini.android.capture.GiniCapture
import net.gini.android.capture.GiniCaptureFragment
import net.gini.android.capture.GiniCaptureHelperForInstrumentationTests
import net.gini.android.capture.di.CaptureSdkIsolatedKoinContext
import net.gini.android.capture.internal.document.ImageMultiPageDocumentMemoryStore
import net.gini.android.capture.internal.network.Configuration
import net.gini.android.capture.internal.network.ConfigurationNetworkResult
import net.gini.android.capture.internal.network.NetworkRequestsManager
import net.gini.android.capture.internal.provider.GiniBankConfigurationProvider
import net.gini.android.capture.tracking.useranalytics.BufferedUserAnalyticsEventTracker
import net.gini.android.capture.tracking.useranalytics.UserAnalytics
import net.gini.android.capture.view.DefaultLoadingIndicatorAdapter
import net.gini.android.capture.view.DefaultNavigationBarTopAdapter
import net.gini.android.capture.view.InjectedViewAdapterInstance
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.dsl.module
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import java.util.UUID

/**
* Integration test to verify the correct behavior of Analytics.
* Classes involved
* - [GiniCaptureFragment]
* - [GiniCapture]
* - [GiniCapture.Internal]
* - [NetworkRequestsManager]
* - [BufferedUserAnalyticsEventTracker]
* */

@RunWith(AndroidJUnit4::class)
class GiniCaptureFragmentTest {
private lateinit var networkRequestsManager: NetworkRequestsManager
private lateinit var giniCapture: GiniCapture
private lateinit var giniInternal: GiniCapture.Internal
private lateinit var memoryStore: ImageMultiPageDocumentMemoryStore
private val koinTestModule = module {
single { GiniBankConfigurationProvider() }
}

/**
* We are using multiple dependencies in different classes, so many mocks are needed
* to fully test the functionality of AnalyticsTracker, by running the GiniCaptureFragment
* in Isolation.
* Mock was needed for [NetworkRequestsManager], [GiniCapture], [GiniCapture.Internal],
* [ImageMultiPageDocumentMemoryStore], [GiniBankConfigurationProvider]
* Also, in [GiniCaptureFragment], we are using koin to update
* the [GiniBankConfigurationProvider], for that we need to load and unload the module,
* other wise an exception will be thrown from koin for not loaded module.
* In the end, we need to set the mocked [GiniCapture] instance, and we have a helper class
* [GiniCaptureHelperForInstrumentationTests] for that.
*
* */

@Before
fun setUp() {
CaptureSdkIsolatedKoinContext.koin.loadModules(listOf(koinTestModule))

networkRequestsManager = mock()
giniCapture = mock()
giniInternal = mock()
memoryStore = mock()

whenever(giniInternal.networkRequestsManager).thenReturn(networkRequestsManager)
whenever(giniCapture.internal()).thenReturn(giniInternal)
whenever(giniCapture.entryPoint).thenReturn(EntryPoint.BUTTON)
whenever(giniInternal.imageMultiPageDocumentMemoryStore).thenReturn(memoryStore)
whenever(giniInternal.navigationBarTopAdapterInstance).thenReturn(
InjectedViewAdapterInstance(DefaultNavigationBarTopAdapter())
)
whenever(giniCapture.documentImportEnabledFileTypes).thenReturn(
DocumentImportEnabledFileTypes.NONE
)
whenever(giniCapture.internal().loadingIndicatorAdapterInstance).thenReturn(
InjectedViewAdapterInstance(DefaultLoadingIndicatorAdapter())
)

GiniCaptureHelperForInstrumentationTests.setGiniCaptureInstance(giniCapture)
}


/**
* Unload the koin modules which were loaded in the [setUp].
* */

@After
fun tearDown() = CaptureSdkIsolatedKoinContext.koin.unloadModules(listOf(koinTestModule))


@Test
fun analyticsTracker_shouldBeEmpty_whenUserJourneyDisabled() {

whenever(networkRequestsManager.getConfigurations(any())).thenReturn(
CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = false))
)

launchGiniCaptureFragment().use { scenario ->

scenario.moveToState(Lifecycle.State.STARTED)

scenario.onFragment { _ ->
assertThat(getAnalyticsTracker().getTrackers()).isEmpty()
}
}
}


@Test
fun analyticsTracker_shouldNotBeEmpty_whenUserJourneyEnabled() {

whenever(networkRequestsManager.getConfigurations(any())).thenReturn(
CompletableFuture.completedFuture(getMockedConfiguration(userJourneyEnabled = true))
)

launchGiniCaptureFragment().use { scenario ->

scenario.moveToState(Lifecycle.State.STARTED)

scenario.onFragment { _ ->
assertThat(getAnalyticsTracker().getTrackers()).isNotEmpty()
}
}
}


private fun getMockedConfiguration(userJourneyEnabled: Boolean): ConfigurationNetworkResult {
val testConfig = Configuration(
id = UUID.randomUUID(),
clientID = TEST_CLIENT_ID,
isUserJourneyAnalyticsEnabled = userJourneyEnabled,
isSkontoEnabled = false,
isReturnAssistantEnabled = false,
isTransactionDocsEnabled = false,
isQrCodeEducationEnabled = false,
isInstantPaymentEnabled = false,
isEInvoiceEnabled = false,
amplitudeApiKey = TEST_API_KEY
)

return ConfigurationNetworkResult(testConfig, UUID.randomUUID())
}

private fun getAnalyticsTracker(): BufferedUserAnalyticsEventTracker {
return UserAnalytics.getAnalyticsEventTracker() as BufferedUserAnalyticsEventTracker
}

/**
* Helper method to launch the [GiniCaptureFragment] in a container,
* needed in all the tests.
*
* */

private fun launchGiniCaptureFragment(): FragmentScenario<GiniCaptureFragment> {
return FragmentScenario.launchInContainer(
fragmentClass = GiniCaptureFragment::class.java,
factory = object : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return GiniCaptureFragment.createInstance().apply {
setListener(mock())
}
}
}
)
}

companion object {
private const val TEST_CLIENT_ID = "test-client-id"
private const val TEST_API_KEY = "test-api-key"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.gini.android.capture.tracking.useranalytics

import android.content.Context
import androidx.annotation.VisibleForTesting
import net.gini.android.capture.internal.network.NetworkRequestsManager
import net.gini.android.capture.internal.provider.UniqueIdProvider
import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsEventProperty
Expand Down Expand Up @@ -64,52 +65,53 @@ internal class BufferedUserAnalyticsEventTracker(
}


override fun setEventSuperProperty(property: Set<UserAnalyticsEventSuperProperty>) {
override fun setEventSuperProperty(property: Set<UserAnalyticsEventSuperProperty>): Boolean {
if (!mIsUserJourneyEnabled)
return
return false
this.eventSuperProperties.add(property)
trySendEvents()
return trySendEvents()
}

override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty) {
setEventSuperProperty(setOf(property))
override fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean {
return setEventSuperProperty(setOf(property))
}

override fun setUserProperty(userProperties: Set<UserAnalyticsUserProperty>) {
override fun setUserProperty(userProperties: Set<UserAnalyticsUserProperty>): Boolean {
if (!mIsUserJourneyEnabled)
return
return false
this.userProperties.add(userProperties)
trySendEvents()
return trySendEvents()
}

override fun setUserProperty(userProperty: UserAnalyticsUserProperty) {
setUserProperty(setOf(userProperty))
override fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean {
return setUserProperty(setOf(userProperty))
}

override fun trackEvent(
eventName: UserAnalyticsEvent,
properties: Set<UserAnalyticsEventProperty>
) {
): Boolean {
if (!mIsUserJourneyEnabled)
return
return false
events.add(Pair(eventName, properties))
trySendEvents()
return trySendEvents()
}

override fun trackEvent(eventName: UserAnalyticsEvent) {
trackEvent(eventName, emptySet())
override fun trackEvent(eventName: UserAnalyticsEvent): Boolean {
return trackEvent(eventName, emptySet())
}

override fun flushEvents() {
amplitude?.flushEvents()
override fun flushEvents(): Boolean {
return amplitude?.let {
amplitude?.flushEvents()
} ?: false
}

private fun trySendEvents() {
if (!mIsUserJourneyEnabled)
return
if (eventTrackers.isEmpty()) {
LOG.debug("No trackers found. Skipping sending events")
return
private fun trySendEvents(): Boolean {
if (!mIsUserJourneyEnabled || eventTrackers.isEmpty()) {
if (eventTrackers.isEmpty())
LOG.debug("No trackers found. Skipping sending events")
return false
}

LOG.debug("${eventTrackers.size} Tracker(s) found. Sending events...")
Expand All @@ -135,10 +137,14 @@ internal class BufferedUserAnalyticsEventTracker(


LOG.debug("Events sent")
return true
}

private fun everyTracker(block: (UserAnalyticsEventTracker) -> Unit) {
eventTrackers.forEach(block)
}

@VisibleForTesting
internal fun getTrackers(): Set<UserAnalyticsEventTracker> = eventTrackers.toSet()

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@ import net.gini.android.capture.tracking.useranalytics.properties.UserAnalyticsU

interface UserAnalyticsEventTracker {

fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty)
fun setEventSuperProperty(property: UserAnalyticsEventSuperProperty): Boolean

fun setEventSuperProperty(property: Set<UserAnalyticsEventSuperProperty>)
fun setEventSuperProperty(property: Set<UserAnalyticsEventSuperProperty>): Boolean

fun setUserProperty(userProperty: UserAnalyticsUserProperty)
fun setUserProperty(userProperty: UserAnalyticsUserProperty): Boolean

fun setUserProperty(userProperties: Set<UserAnalyticsUserProperty>)
fun setUserProperty(userProperties: Set<UserAnalyticsUserProperty>): Boolean

fun trackEvent(eventName: UserAnalyticsEvent)
fun trackEvent(eventName: UserAnalyticsEvent): Boolean

fun trackEvent(eventName: UserAnalyticsEvent, properties: Set<UserAnalyticsEventProperty>)
fun trackEvent(
eventName: UserAnalyticsEvent,
properties: Set<UserAnalyticsEventProperty>
): Boolean

fun flushEvents()
fun flushEvents(): Boolean
}


Expand Down
Loading