Skip to content

Commit

Permalink
Merge pull request #17 from CAMOBAP/bugfix/refactor-android-ui-tests
Browse files Browse the repository at this point in the history
Bugfix/refactor android UI tests
  • Loading branch information
e271828- authored Jan 29, 2022
2 parents 9622273 + 08491a7 commit c0c5f7d
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 49 deletions.
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ name: Android SDK CI
on: push
jobs:
build:
name: Build & Test
name: Build & Unit-test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-java@v1
- uses: actions/setup-java@v2
with:
java-version: '1.8'
java-version: '8'
distribution: adopt
cache: 'gradle'
- name: Assemble & Test
run: ./gradlew build --stacktrace
Expand All @@ -18,3 +19,25 @@ jobs:
jshint --extract=always sdk/src/main/assets/hcaptcha-form.html
- name: JitPack Test
run: ./gradlew publishReleasePublicationToMavenLocal
ui-tests:
name: Android UI Tests
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
api-level: [29] # , 23, 21]
target: [default] #, google_apis]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v2
with:
java-version: '8'
distribution: adopt
cache: 'gradle'
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedCheck
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'com.android.tools.build:gradle:4.1.3'

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
Expand All @@ -15,6 +15,6 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
12 changes: 7 additions & 5 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ android {
// See https://developer.android.com/studio/publish/versioning
// versionCode must be integer and be incremented by one for every new update
// android system uses this to prevent downgrades
versionCode 7
versionCode 8

// version number visible to the user
// should follow semantic versioning (See https://semver.org)
versionName "1.2.0"
versionName "1.3.0"

buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\""

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
Expand All @@ -40,19 +42,19 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.1'
implementation 'org.projectlombok:lombok:1.18.16'
compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'

testImplementation 'junit:junit:4.13.1'
testImplementation 'org.mockito:mockito-core:3.6.28'
testImplementation 'org.skyscreamer:jsonassert:1.5.0'


androidTestImplementation 'androidx.test:core:1.3.0'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-web:3.3.0'
androidTestImplementation 'org.hamcrest:hamcrest-library:1.3'

debugImplementation 'androidx.fragment:fragment-testing:1.3.0'
Expand Down
39 changes: 39 additions & 0 deletions sdk/src/androidTest/assets/hcaptcha-form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
</head>
<body>
<div id="hcaptcha-container"></div>

<input id="input-text" />

<button id="on-error" onclick="onError()">Error</button>
<button id="on-pass" onclick="onPass()">Pass</button>

<script type="text/javascript">
var DI = window.JSDI.getDebugInfo();
var BridgeObject = window.JSInterface;
var bridgeConfig = JSON.parse(BridgeObject.getConfig());

function onHcaptchaLoaded() {
try {
BridgeObject.onLoaded();
} catch (e) {
BridgeObject.onError(29);
}
}

function onPass() {
const token = document.getElementById("input-text").value;
BridgeObject.onPass(token);
}

function onError() {
const errorCode = parseInt(document.getElementById("input-text").value);
BridgeObject.onError(errorCode);
}

onHcaptchaLoaded();
</script>
</body>
</html>
47 changes: 40 additions & 7 deletions sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,40 @@
import androidx.test.espresso.PerformException;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.util.HumanReadables;
import androidx.test.espresso.util.TreeIterables;
import org.hamcrest.Matcher;

import java.util.concurrent.TimeoutException;

import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static org.hamcrest.CoreMatchers.any;
import static org.hamcrest.CoreMatchers.not;

public class AssertUtil {

public static ViewAction waitToBeDisplayed(final int viewId, final long millis) {
public static ViewAction waitToBeDisplayed(final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isRoot();
return any(View.class);
}

@Override
public String getDescription() {
return "wait for view (id: " + viewId + ") for " + millis + " ms";
return "wait for view displayed for " + millis + " ms";
}

@Override
public void perform(final UiController uiController, final View view) {
uiController.loopMainThreadUntilIdle();
final long startTime = System.currentTimeMillis();
final long endTime = startTime + millis;
final Matcher<View> viewMatcher = withId(viewId);
final long endTime = System.currentTimeMillis() + millis;
final Matcher<View> viewIsDisplayed = isDisplayed();
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
if (viewMatcher.matches(child) && viewIsDisplayed.matches(child)) {
if (viewIsDisplayed.matches(child)) {
return;
}
}
Expand All @@ -51,4 +53,35 @@ public void perform(final UiController uiController, final View view) {
};
}

public static ViewAction waitToDisappear(final long millis) {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return any(View.class);
}

@Override
public String getDescription() {
return "wait for view to be gone for " + millis + " ms ";
}

@Override
public void perform(final UiController uiController, final View view) {
long endTime = System.currentTimeMillis() + millis;

do {
if (view.getVisibility() == View.GONE || view.getVisibility() == View.INVISIBLE) {
return;
}
uiController.loopMainThreadForAtLeast(50);
} while (System.currentTimeMillis() < endTime);

throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(new TimeoutException())
.build();
}
};
}
}
Original file line number Diff line number Diff line change
@@ -1,69 +1,141 @@
package com.hcaptcha.sdk;

import android.os.Bundle;
import android.webkit.WebView;
import android.util.Log;

import androidx.fragment.app.testing.FragmentScenario;
import androidx.test.espresso.web.webdriver.DriverAtoms;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.jetbrains.annotations.NotNull;

import org.junit.Test;
import org.junit.runner.RunWith;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.*;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.clearElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static com.hcaptcha.sdk.AssertUtil.waitToBeDisplayed;
import static com.hcaptcha.sdk.AssertUtil.waitToDisappear;
import static com.hcaptcha.sdk.HCaptchaDialogFragment.KEY_CONFIG;
import static com.hcaptcha.sdk.HCaptchaDialogFragment.KEY_LISTENER;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;


@RunWith(AndroidJUnit4.class)
public class HCaptchaDialogFragmentTest {
public class HCaptchaDialogTestAdapter extends HCaptchaDialogListener {
@Override
void onSuccess(HCaptchaTokenResponse hCaptchaTokenResponse) {
}

@Override
void onFailure(HCaptchaException hCaptchaException) {
}
};

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment() {
return launchCaptchaFragment(true);
}

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader) {
return launchCaptchaFragment(showLoader, new HCaptchaDialogTestAdapter());
}

public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(HCaptchaDialogListener listener) {
return launchCaptchaFragment(true, listener);
}

public FragmentScenario<HCaptchaDialogFragment> getTestScenario() {
public FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader, HCaptchaDialogListener listener) {
final HCaptchaConfig hCaptchaConfig = HCaptchaConfig.builder()
.siteKey("10000000-ffff-ffff-ffff-000000000001")
.endpoint("https://js.hcaptcha.com/1/api.js")
.locale("en")
.loading(showLoader)
.size(HCaptchaSize.INVISIBLE)
.theme(HCaptchaTheme.LIGHT)
.build();
final Bundle args = new Bundle();
args.putSerializable(KEY_CONFIG, hCaptchaConfig);
args.putParcelable(KEY_LISTENER, new HCaptchaDialogListener() {
@Override
void onSuccess(HCaptchaTokenResponse hCaptchaTokenResponse) {
}

@Override
void onFailure(HCaptchaException hCaptchaException) {
}
});
return FragmentScenario.launch(HCaptchaDialogFragment.class, args);
args.putParcelable(KEY_LISTENER, listener);
return FragmentScenario.launchInContainer(HCaptchaDialogFragment.class, args);
}

@Test
public void loader_is_visible_while_webview_is_loading() {
getTestScenario();
public void loaderVisible() {
launchCaptchaFragment();
onView(withId(R.id.loadingContainer)).check(matches(isDisplayed()));
onView(withId(R.id.webView)).check(matches(not(isDisplayed())));
onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000));
onView(withId(R.id.loadingContainer)).perform(waitToDisappear(10000));
}

@Test
public void loaderDisabled() {
launchCaptchaFragment(false);
onView(withId(R.id.loadingContainer)).check(matches(not(isDisplayed())));
onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000));
}

@Test
public void webview_is_visible_after_loading() {
final FragmentScenario<HCaptchaDialogFragment> scenario = getTestScenario();
scenario.onFragment(new FragmentScenario.FragmentAction<HCaptchaDialogFragment>() {
public void webViewReturnToken() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
final HCaptchaDialogListener listener = new HCaptchaDialogTestAdapter() {
@Override
public void perform(@NotNull HCaptchaDialogFragment fragment) {
assertNotNull(fragment.getDialog());
WebView.setWebContentsDebuggingEnabled(true);
fragment.onLoaded();
void onSuccess(HCaptchaTokenResponse hCaptchaTokenResponse) {
assertEquals("test-token", hCaptchaTokenResponse.getTokenResult());
latch.countDown();
}
});
onView(withId(R.id.loadingContainer)).check(matches(not(isDisplayed())));
onView(withId(R.id.webView)).check(matches(isDisplayed()));
onView(isRoot()).perform(waitToBeDisplayed(R.id.webView, 1000));
};

final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(listener);
onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000));

onWebView(withId(R.id.webView)).forceJavascriptEnabled();

onWebView().withElement(findElement(Locator.ID, "input-text"))
.perform(clearElement())
.perform(DriverAtoms.webKeys("test-token"));

onWebView().withElement(findElement(Locator.ID, "on-pass"))
.perform(webClick());

assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback
}

@Test
public void webViewReturnsError() throws Exception {
CountDownLatch latch = new CountDownLatch(1);
final HCaptchaDialogListener listener = new HCaptchaDialogTestAdapter() {
@Override
void onFailure(HCaptchaException hCaptchaException) {
assertEquals(HCaptchaError.SESSION_TIMEOUT, hCaptchaException.getHCaptchaError());
latch.countDown();
}
};

final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(listener);
onView(withId(R.id.webView)).perform(waitToBeDisplayed(1000));

onWebView(withId(R.id.webView)).forceJavascriptEnabled();

onWebView().withElement(findElement(Locator.ID, "input-text"))
.perform(clearElement())
.perform(DriverAtoms.webKeys(
String.valueOf(HCaptchaError.SESSION_TIMEOUT.getErrorId())));

onWebView().withElement(findElement(Locator.ID, "on-error"))
.perform(webClick());

assertTrue(latch.await(1000, TimeUnit.MILLISECONDS)); // wait for callback
}
}
Loading

0 comments on commit c0c5f7d

Please sign in to comment.