Skip to content

Commit

Permalink
fix(sdk): do not intercept touch input when loading is hidden (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
CAMOBAP authored May 25, 2023
1 parent 0ee0816 commit 4005f94
Show file tree
Hide file tree
Showing 9 changed files with 226 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ public void onMarkUsed(final View v) {
}
}

public void onHitTest(final View v) {
Toast.makeText(this, "Hit Test!", Toast.LENGTH_SHORT).show();
Log.d(TAG, "onHitTest");
}

private void setupClient(final HCaptcha hCaptcha) {
hCaptcha
.addOnSuccessListener(response -> {
Expand Down
13 changes: 13 additions & 0 deletions example-app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,17 @@
android:layout_height="wrap_content" />
</LinearLayout>

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hit_test"
android:layout_centerInParent="true"
android:onClick="onHitTest"/>

</RelativeLayout>

</LinearLayout>
1 change: 1 addition & 0 deletions example-app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
<string name="web_view_debug">Web Debug</string>
<string name="hw_accel">Disable HW Accel</string>
<string name="hide_dialog">Hide Dialog</string>
<string name="hit_test">Hit test</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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.failAsNonReachable;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewErrorByInput;
import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken;
import static com.hcaptcha.sdk.AssertUtil.waitToBeDisplayed;
Expand All @@ -18,15 +17,21 @@
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.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.app.Dialog;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.DisplayMetrics;
import android.view.InflateException;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import androidx.fragment.app.testing.FragmentScenario;
import androidx.lifecycle.Lifecycle;
import androidx.test.core.app.ActivityScenario;
Expand Down Expand Up @@ -58,34 +63,54 @@ public class HCaptchaDialogFragmentTest {
.htmlProvider(new HCaptchaTestHtml())
.build();

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

private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(boolean showLoader) {
return launchCaptchaFragment(config.toBuilder().loading(showLoader).build(), new HCaptchaStateTestAdapter());
private FragmentScenario<HCaptchaDialogFragment> launchInContainer(boolean showLoader) {
return launchInContainer(config.toBuilder().loading(showLoader).build(), new HCaptchaStateTestAdapter());
}

private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(HCaptchaStateListener listener) {
return launchCaptchaFragment(config, listener);
private FragmentScenario<HCaptchaDialogFragment> launchInContainer(HCaptchaStateListener listener) {
return launchInContainer(config, listener);
}

private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(final HCaptchaConfig captchaConfig,
HCaptchaStateListener listener) {
return launchCaptchaFragment(captchaConfig, listener, Lifecycle.State.RESUMED);
private FragmentScenario<HCaptchaDialogFragment> launchInContainer(final HCaptchaConfig captchaConfig,
final HCaptchaStateListener listener) {
return launchInContainer(captchaConfig, internalConfig, listener);
}

private FragmentScenario<HCaptchaDialogFragment> launchCaptchaFragment(final HCaptchaConfig captchaConfig,
HCaptchaStateListener listener,
Lifecycle.State initialState) {
private FragmentScenario<HCaptchaDialogFragment> launchInContainer(
final HCaptchaConfig captchaConfig,
final HCaptchaInternalConfig internalCaptchaConfig,
final HCaptchaStateListener listener) {
return launchInContainer(captchaConfig, internalCaptchaConfig, listener, Lifecycle.State.RESUMED);
}

private FragmentScenario<HCaptchaDialogFragment> launchInContainer(
final HCaptchaConfig captchaConfig,
final HCaptchaInternalConfig internalCaptchaConfig,
final HCaptchaStateListener listener,
final Lifecycle.State initialState) {
final Bundle args = new Bundle();
args.putSerializable(KEY_CONFIG, captchaConfig);
args.putSerializable(KEY_INTERNAL_CONFIG, internalConfig);
args.putSerializable(KEY_INTERNAL_CONFIG, internalCaptchaConfig);
args.putParcelable(KEY_LISTENER, listener);
return FragmentScenario.launchInContainer(HCaptchaDialogFragment.class,
args, R.style.HCaptchaDialogTheme, initialState);
}

private FragmentScenario<HCaptchaDialogFragment> launch(
final HCaptchaConfig captchaConfig,
final HCaptchaInternalConfig internalCaptchaConfig,
final HCaptchaStateListener listener) {
final Bundle args = new Bundle();
args.putSerializable(KEY_CONFIG, captchaConfig);
args.putSerializable(KEY_INTERNAL_CONFIG, internalCaptchaConfig);
args.putParcelable(KEY_LISTENER, listener);
return FragmentScenario.launch(HCaptchaDialogFragment.class, args);
}

private void waitForWebViewToEmitToken(final CountDownLatch latch)
throws InterruptedException {
onView(withId(R.id.webView)).perform(waitToBeDisplayed());
Expand All @@ -104,7 +129,7 @@ private void waitForWebViewToEmitToken(final CountDownLatch latch)

@Test
public void loaderVisible() {
launchCaptchaFragment();
launchInContainer();
onView(withId(R.id.loadingContainer)).check(matches(isDisplayed()));
onView(withId(R.id.webView)).perform(waitToBeDisplayed());
final long waitToDisappearMs = 10000;
Expand All @@ -113,7 +138,7 @@ public void loaderVisible() {

@Test
public void loaderDisabled() {
launchCaptchaFragment(false);
launchInContainer(false);
onView(withId(R.id.loadingContainer)).check(matches(not(isDisplayed())));
onView(withId(R.id.webView)).perform(waitToBeDisplayed());
}
Expand All @@ -129,7 +154,7 @@ void onSuccess(String token) {
}
};

launchCaptchaFragment(listener);
launchInContainer(listener);
waitForWebViewToEmitToken(latch);
}

Expand All @@ -144,22 +169,27 @@ void onFailure(HCaptchaException exception) {
}
};

launchCaptchaFragment(listener);
launchInContainer(listener);

waitHCaptchaWebViewErrorByInput(latch, HCaptchaError.CHALLENGE_ERROR, AWAIT_CALLBACK_MS);
}

@Test
public void onOpenCallbackWorks() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
final CountDownLatch latch = new CountDownLatch(2);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {
@Override
void onOpen() {
latch.countDown();
}

@Override
void onSuccess(String response) {
latch.countDown();
}
};

launchCaptchaFragment(listener);
launchInContainer(listener);
waitForWebViewToEmitToken(latch);
}

Expand Down Expand Up @@ -222,11 +252,6 @@ public void testRetryPredicate() throws Exception {
void onSuccess(String token) {
successLatch.countDown();
}

@Override
void onFailure(HCaptchaException exception) {
failAsNonReachable();
}
};

final HCaptchaConfig updatedWithRetry = config.toBuilder()
Expand All @@ -236,7 +261,7 @@ void onFailure(HCaptchaException exception) {
})
.build();

launchCaptchaFragment(updatedWithRetry, listener);
launchInContainer(updatedWithRetry, listener);

waitHCaptchaWebViewErrorByInput(failureLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

Expand All @@ -249,11 +274,6 @@ public void testNotRetryPredicate() throws Exception {
final CountDownLatch retryLatch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {

@Override
void onSuccess(String token) {
failAsNonReachable();
}

@Override
void onFailure(HCaptchaException exception) {
failureLatch.countDown();
Expand All @@ -267,7 +287,7 @@ void onFailure(HCaptchaException exception) {
})
.build();

launchCaptchaFragment(updatedWithRetry, listener);
launchInContainer(updatedWithRetry, listener);

waitHCaptchaWebViewErrorByInput(retryLatch, HCaptchaError.NETWORK_ERROR, AWAIT_CALLBACK_MS);

Expand All @@ -285,7 +305,7 @@ void onSuccess(String token) {
}
};

final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(config, listener);
final FragmentScenario<HCaptchaDialogFragment> scenario = launchInContainer(config, listener);
scenario.moveToState(Lifecycle.State.STARTED).moveToState(Lifecycle.State.RESUMED);

waitHCaptchaWebViewToken(successLatch, AWAIT_CALLBACK_MS);
Expand All @@ -304,7 +324,7 @@ void onSuccess(String token) {
}
};

final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(config, listener);
final FragmentScenario<HCaptchaDialogFragment> scenario = launchInContainer(config, listener);
scenario.recreate();

waitHCaptchaWebViewToken(successLatch, AWAIT_CALLBACK_MS);
Expand All @@ -328,12 +348,82 @@ public void testVerifyOnStoppedFragmentNoException() throws InterruptedException

@Test(expected = IllegalArgumentException.class)
public void testReset() {
final FragmentScenario<HCaptchaDialogFragment> scenario = launchCaptchaFragment(
final FragmentScenario<HCaptchaDialogFragment> scenario = launchInContainer(
config, new HCaptchaStateTestAdapter());

scenario.onFragment(HCaptchaDialogFragment::reset);

// The fragment has been removed from the FragmentManager already.
scenario.onFragment(fragment -> assertTrue(fragment.isDetached()));
}

@Test
public void testBackShouldCloseCaptcha() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() {
@Override
void onFailure(HCaptchaException exception) {
assertEquals(HCaptchaError.CHALLENGE_CLOSED, exception.getHCaptchaError());
latch.countDown();
}
};

try (FragmentScenario<HCaptchaDialogFragment> scenario = launch(config, internalConfig, listener)) {
scenario.onFragment(fragment -> {
final Dialog dialog = fragment.getDialog();
assertNotNull(dialog);
assertTrue(dialog.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)));
assertTrue(dialog.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)));
});
}

assertTrue(latch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS));
}

@Test
public void testBackShouldNotCloseCaptchaWithoutDefaultLoadingIndicator() {
try (FragmentScenario<HCaptchaDialogFragment> scenario = launch(
config.toBuilder()
.loading(false)
.build(),
internalConfig.toBuilder()
.htmlProvider(new HCaptchaTestHtml(false))
.build(),
new HCaptchaStateTestAdapter())) {

scenario.onFragment(fragment -> {
final Dialog dialog = fragment.getDialog();
assertNotNull(dialog);
final KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
assertTrue(dialog.dispatchKeyEvent(keyEvent));
});
}
}

@Test
public void testTouchShouldNotCloseCaptchaWithoutDefaultLoadingIndicator() {
try (FragmentScenario<HCaptchaDialogFragment> scenario = launch(
config.toBuilder()
.loading(false)
.build(),
internalConfig.toBuilder()
.htmlProvider(new HCaptchaTestHtml(false))
.build(),
new HCaptchaStateTestAdapter())) {

scenario.onFragment(fragment -> {
final Dialog dialog = fragment.getDialog();
assertNotNull(dialog);
final DisplayMetrics dm = new DisplayMetrics();
final MotionEvent keyEvent = MotionEvent.obtain(
SystemClock.uptimeMillis() - 1,
SystemClock.uptimeMillis(),
MotionEvent.ACTION_DOWN,
dm.widthPixels / 2f,
dm.heightPixels / 2f,
0 /* NO_META */);
assertTrue(dialog.dispatchTouchEvent(keyEvent));
});
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.hcaptcha.sdk;

import static com.hcaptcha.sdk.AssertUtil.failAsNonReachable;

public class HCaptchaStateTestAdapter extends HCaptchaStateListener {
@Override
void onOpen() {
Expand All @@ -8,11 +10,11 @@ void onOpen() {

@Override
void onSuccess(String response) {
// empty default implementation to reduce amount of boilerplate code in tests
failAsNonReachable();
}

@Override
void onFailure(HCaptchaException exception) {
// empty default implementation to reduce amount of boilerplate code in tests
failAsNonReachable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

public class HCaptchaTest {
private static final long AWAIT_CALLBACK_MS = 5000;
private static final long E2E_AWAIT_CALLBACK_MS = AWAIT_CALLBACK_MS * 2;
private static final long E2E_AWAIT_CALLBACK_MS = AWAIT_CALLBACK_MS * 5;

@Rule
public ActivityScenarioRule<TestActivity> rule = new ActivityScenarioRule<>(TestActivity.class);
Expand Down
12 changes: 11 additions & 1 deletion sdk/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@

class HCaptchaTestHtml implements IHCaptchaHtmlProvider {

private final boolean callBridgeOnLoaded;

HCaptchaTestHtml() {
this(true);
}

HCaptchaTestHtml(boolean callBridgeOnLoaded) {
this.callBridgeOnLoaded = callBridgeOnLoaded;
}

@Override
@NonNull
public String getHtml() {
Expand Down Expand Up @@ -41,7 +51,7 @@ public String getHtml() {
+ " const errorCode = arg || parseInt(document.getElementById(\"input-text\").value);\n"
+ " BridgeObject.onError(errorCode);\n"
+ " }\n"
+ " onHcaptchaLoaded();\n"
+ (callBridgeOnLoaded ? "onHcaptchaLoaded();\n" : "")
+ " </script>\n"
+ "</body>\n"
+ "</html>\n";
Expand Down
Loading

0 comments on commit 4005f94

Please sign in to comment.