diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 19071cfe..b5b5e102 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
@@ -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
diff --git a/build.gradle b/build.gradle
index 60279d70..ce622ead 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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
@@ -15,6 +15,6 @@ buildscript {
allprojects {
repositories {
google()
- jcenter()
+ mavenCentral()
}
}
diff --git a/sdk/build.gradle b/sdk/build.gradle
index 8e4c4096..d6eb8e56 100644
--- a/sdk/build.gradle
+++ b/sdk/build.gradle
@@ -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"
@@ -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'
diff --git a/sdk/src/androidTest/assets/hcaptcha-form.html b/sdk/src/androidTest/assets/hcaptcha-form.html
new file mode 100644
index 00000000..fe3818cb
--- /dev/null
+++ b/sdk/src/androidTest/assets/hcaptcha-form.html
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java b/sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java
index c2de9b66..c9309046 100644
--- a/sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java
+++ b/sdk/src/androidTest/java/com/hcaptcha/sdk/AssertUtil.java
@@ -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 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 viewMatcher = withId(viewId);
+ final long endTime = System.currentTimeMillis() + millis;
final Matcher viewIsDisplayed = isDisplayed();
do {
for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
- if (viewMatcher.matches(child) && viewIsDisplayed.matches(child)) {
+ if (viewIsDisplayed.matches(child)) {
return;
}
}
@@ -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 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();
+ }
+ };
+ }
}
diff --git a/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java b/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java
index 3c2102a4..ed6a07ba 100644
--- a/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java
+++ b/sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java
@@ -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 launchCaptchaFragment() {
+ return launchCaptchaFragment(true);
+ }
+
+ public FragmentScenario launchCaptchaFragment(boolean showLoader) {
+ return launchCaptchaFragment(showLoader, new HCaptchaDialogTestAdapter());
+ }
+
+ public FragmentScenario launchCaptchaFragment(HCaptchaDialogListener listener) {
+ return launchCaptchaFragment(true, listener);
+ }
- public FragmentScenario getTestScenario() {
+ public FragmentScenario 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 scenario = getTestScenario();
- scenario.onFragment(new FragmentScenario.FragmentAction() {
+ 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 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 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
+ }
}
diff --git a/sdk/src/main/assets/hcaptcha-form.html b/sdk/src/main/assets/hcaptcha-form.html
index 002858b5..88c23bcc 100644
--- a/sdk/src/main/assets/hcaptcha-form.html
+++ b/sdk/src/main/assets/hcaptcha-form.html
@@ -28,6 +28,7 @@