Skip to content

Commit

Permalink
Automating analytics for UI components (#35)
Browse files Browse the repository at this point in the history
* Add analyticsMode[auto, manual]

* Add album_id to PXLPhoto

* Add auto-analytics OpenLightbox to PDP

* Remove PXLClient.initialize(..) to keep using the same one in Application level

* Fix typo

* Improve CoroutineScope not to be affected by any environment

* Add opendWidget and widgetVisible (incomplete)

* Add all analytics events

* Add AnalyticsObserver for UI testing

* Add UI testing for analytics

* Add analytics' observer

* Update document

* Update document

* Update document

* Update document

* Update document

* Update document

* Remove unused libraries

* Remove unused codes
  • Loading branch information
SungjunApp authored Jan 19, 2021
1 parent 09a07f9 commit 5353c91
Show file tree
Hide file tree
Showing 36 changed files with 798 additions and 223 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,5 @@ fastlane/readme.md
video-player-visibility-utils/
video-player-manager/
JZVideo/

runTestLoop.sh
14 changes: 13 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,24 @@ repositories {
jcenter()
}

tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation 'junit:junit:4.12'

/**
* testing
*/
androidTestImplementation 'androidx.test.ext:junit-ktx:' + rootProject.extJUnitVersion
androidTestImplementation 'androidx.test.espresso:espresso-contrib:' + rootProject.espressoVersion
androidTestImplementation "com.jakewharton.espresso:okhttp3-idling-resource:$jakeWhartonOkhttpOdlingResource"

implementation "androidx.appcompat:appcompat:$androidxAppcompat"
implementation "androidx.constraintlayout:constraintlayout:$androidxConstraint"
implementation "com.google.android.material:material:$material"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package com.pixlee.pixleeandroidsdk

import android.view.View
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.actionWithAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.pixlee.pixleeandroidsdk.tool.MyViewAction
import com.pixlee.pixleeandroidsdk.tool.OkHttpIdlingResourceRule
import com.pixlee.pixleeandroidsdk.tool.ScrollToBottomAction
import com.pixlee.pixleesdk.data.api.AnalyticsEvents
import com.pixlee.pixleesdk.ui.viewholder.PXLPhotoViewHolder
import org.hamcrest.Matcher
import org.hamcrest.StringDescription
import org.hamcrest.core.StringContains
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith


/**
* A test using the androidx.test unified API, which can execute on an Android device or locally using Robolectric.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
@LargeTest
class AnalyticsFragmentTest {
/**
* Use [ActivityScenarioRule] to create and launch the activity under test before each test,
* and close it after each test. This is a replacement for
* [androidx.test.rule.ActivityTestRule].
*/
@get:Rule
var activityScenarioRule = activityScenarioRule<MainActivity>()

@get:Rule
var rule = OkHttpIdlingResourceRule()


@Test
fun testList() {
val buttonId = R.id.tvDebugText

onView(withId(R.id.btKtxAlbumList)).perform(waitUntil(isDisplayed())).perform(ViewActions.click())
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.openedWidget))))
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.widgetVisible))))
onView(withId(buttonId)).check(matches(withText(StringContains.containsString(AnalyticsEvents.widgetVisible))))
onView(withId(R.id.pxlPhotoRecyclerView)).perform(ScrollToBottomAction())
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.loadMore))))
}

@Test
fun testGrid() {
val buttonId = R.id.tvDebugText

onView(withId(R.id.btKtxAlbumGrid)).perform(waitUntil(isDisplayed())).perform(ViewActions.click())
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.openedWidget))))
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.widgetVisible))))
}

@Test
fun testProductDetail() {
val buttonId = R.id.tvDebugText
onView(withId(R.id.btKtxAlbumList)).perform(waitUntil(isDisplayed())).perform(ViewActions.click())
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.openedWidget))))
onView(withId(buttonId)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.widgetVisible))))
onView(withId(R.id.pxlPhotoRecyclerView)).perform(RecyclerViewActions.actionOnItemAtPosition<PXLPhotoViewHolder>(0, MyViewAction.clickChildViewWithId(R.id.vListRoot)))
//Thread.sleep(1000)

onView(withId(R.id.tvDebugTextViewer)).perform(waitUntil(isDisplayed())).check(matches(withText(StringContains.containsString(AnalyticsEvents.openedLightbox))))
}


fun waitUntil(matcher: Matcher<View>): ViewAction {
return actionWithAssertions(object : ViewAction {
override fun getConstraints(): Matcher<View> {
return isAssignableFrom(View::class.java)
}

override fun getDescription(): String {
val description = StringDescription()
matcher.describeTo(description)
return String.format("wait until: %s", description)
}

override fun perform(uiController: UiController, view: View) {
if (!matcher.matches(view)) {
val callback = LayoutChangeCallback(matcher)
try {
IdlingRegistry.getInstance().register(callback)
view.addOnLayoutChangeListener(callback)
uiController.loopMainThreadUntilIdle()
} finally {
view.removeOnLayoutChangeListener(callback)
IdlingRegistry.getInstance().unregister(callback)
}
}
}
})
}

private class LayoutChangeCallback constructor(val matcher: Matcher<View>) : IdlingResource, View.OnLayoutChangeListener {
private var callback: IdlingResource.ResourceCallback? = null
private var matched = false
override fun getName(): String {
return "Layout change callback"
}

override fun isIdleNow(): Boolean {
return matched
}

override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
this.callback = callback
}

override fun onLayoutChange(v: View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
matched = matcher.matches(v)
callback!!.onTransitionToIdle()
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.pixlee.pixleeandroidsdk.tool;

import android.view.View;

import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;

import org.hamcrest.Matcher;

/**
* Created by sungjun on 1/15/21.
*/
public class MyViewAction {

public static ViewAction clickChildViewWithId(final int id) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return null;
}

@Override
public String getDescription() {
return "Click on a child view with specified id.";
}

@Override
public void perform(UiController uiController, View view) {
View v = view.findViewById(id);
v.performClick();
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.pixlee.pixleeandroidsdk.tool

import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import com.jakewharton.espresso.OkHttp3IdlingResource
import com.pixlee.pixleesdk.network.NetworkModule
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

/**
* Created by sungjun on 1/15/21.
*/

class OkHttpIdlingResourceRule: TestRule {
private val resource : IdlingResource = OkHttp3IdlingResource.create("OkHttp", NetworkModule.provideOkHttpClient())

override fun apply(base: Statement, description: Description): Statement {
return object: Statement() {
override fun evaluate() {
IdlingRegistry.getInstance().register(resource)
base.evaluate()
IdlingRegistry.getInstance().unregister(resource)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.pixlee.pixleeandroidsdk.tool

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.Matcher
import org.hamcrest.Matchers

/**
* Created by sungjun on 1/15/21.
*/
class ScrollToBottomAction : ViewAction {
override fun getDescription(): String {
return "scroll RecyclerView to bottom"
}

override fun getConstraints(): Matcher<View> {
return Matchers.allOf<View>(ViewMatchers.isAssignableFrom(RecyclerView::class.java), ViewMatchers.isDisplayed())
}

override fun perform(uiController: UiController?, view: View?) {
val recyclerView = view as RecyclerView
val itemCount = recyclerView.adapter?.itemCount
val position = itemCount?.minus(1) ?: 0
recyclerView.scrollToPosition(position)
uiController?.loopMainThreadUntilIdle()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ public class MainActivity extends BaseActivity {
final String TAG = "MainActivity";
public int frameLayoutId = R.id.contentFrame;

ActivityMainBinding binding;
public ActivityMainBinding binding;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

setSupportActionBar(binding.toolbar);

if (binding.toolbar.getNavigationIcon() != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package com.pixlee.pixleeandroidsdk.ui

import android.graphics.Color
import android.util.Log
import android.util.TypedValue
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixlee.pixleeandroidsdk.Event
import com.pixlee.pixleesdk.util.Event
import com.pixlee.pixleesdk.client.PXLKtxAlbum
import com.pixlee.pixleesdk.client.PXLKtxBaseAlbum
import com.pixlee.pixleesdk.data.PXLPhoto
import com.pixlee.pixleesdk.enums.PXLPhotoSize
import com.pixlee.pixleesdk.ui.viewholder.PhotoWithImageScaleType
import com.pixlee.pixleesdk.ui.widgets.ImageScaleType
import com.pixlee.pixleesdk.ui.widgets.PXLPhotoView
import com.pixlee.pixleesdk.ui.widgets.TextPadding
import com.pixlee.pixleesdk.ui.widgets.TextViewStyle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.core.view.GravityCompat
import androidx.core.widget.NestedScrollView
import com.google.android.material.radiobutton.MaterialRadioButton
import com.pixlee.pixleeandroidsdk.BuildConfig
import com.pixlee.pixleeandroidsdk.EventObserver
import com.pixlee.pixleesdk.util.EventObserver
import com.pixlee.pixleeandroidsdk.R
import com.pixlee.pixleeandroidsdk.ui.BaseFragment
import com.pixlee.pixleeandroidsdk.ui.BaseViewModel
Expand Down Expand Up @@ -213,10 +213,10 @@ class KtxAnalyticsFragment : BaseFragment() {
widgetVisible = true
}
if (!pxlPhotoView.getLocalVisibleRect(scrollBounds)
|| scrollBounds.height() < pxlPhotoView.getHeight()) {
Log.i("PXLAnalytics", "BTN APPEAR PARCIALY")
|| scrollBounds.height() < pxlPhotoView.height) {
Log.i("PXLAnalytics", "btn appears partially")
} else {
Log.i("PXLAnalytics", "BTN APPEAR FULLY!!!")
Log.i("PXLAnalytics", "btn appears FULLY!!!")
}
}
}
Expand Down
Loading

0 comments on commit 5353c91

Please sign in to comment.