diff --git a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java index 130b1c6a..1457714f 100644 --- a/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java +++ b/sdk/src/main/java/com/hcaptcha/sdk/HCaptchaDialogFragment.java @@ -67,6 +67,10 @@ public final class HCaptchaDialogFragment extends DialogFragment implements IHCa private boolean readyForInteraction = false; + private boolean webViewLoaded = false; + + private boolean replayLoadedOnStart = false; + @Nullable private static HCaptchaWebView sPreloadWebView; @@ -197,6 +201,7 @@ public void onDestroy() { HCaptchaLog.d("DialogFragment.onDestroy"); super.onDestroy(); readyForInteraction = false; + replayLoadedOnStart = webViewLoaded; if (webViewHelper != null) { webViewHelper.reset(); } @@ -216,7 +221,8 @@ public void onStart() { window.setDimAmount(0); } } - if (!readyForInteraction && webViewHelper != null) { + if (!readyForInteraction && webViewHelper != null && replayLoadedOnStart) { + replayLoadedOnStart = false; HCaptchaLog.d("DialogFragment.onStart: re-triggering onLoaded after reset"); onLoaded(); } @@ -258,6 +264,7 @@ public void onLoaded() { HCaptchaLog.w("DialogFragment.onLoaded webViewHelper == null, likely about to destroy"); return; } + webViewLoaded = true; if (listener != null) { listener.onLoaded(); } @@ -371,6 +378,8 @@ public void reset() { if (webViewHelper != null) { webViewHelper.reset(); } + webViewLoaded = false; + replayLoadedOnStart = false; if (isAdded()) { dismissAllowingStateLoss(); } diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java index 1fc8e0cb..c02a7b74 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaDialogFragmentTest.java @@ -3,6 +3,7 @@ import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.intent.Intents.intended; import static androidx.test.espresso.intent.Intents.intending; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction; import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData; @@ -14,8 +15,8 @@ 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 androidx.test.espresso.intent.Intents.intended; import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; +import static com.hcaptcha.sdk.AssertUtil.evaluateJavascript; import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewErrorByInput; import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken; import static com.hcaptcha.sdk.AssertUtil.waitToBeDisplayed; @@ -157,11 +158,17 @@ private void waitForWebViewToEmitToken(final CountDownLatch latch) @Test public void loaderVisible() { - launchInContainer(); + launchInContainer( + config, + internalConfig.toBuilder() + .htmlProvider(new HCaptchaTestHtml(false)) + .build(), + new HCaptchaStateTestAdapter()); onView(withId(R.id.loadingContainer)).perform(waitToBeDisplayed()); onView(withId(R.id.loadingContainer)).check(matches(isDisplayed())); onView(withId(R.id.loadingContainer)).check(matches(withBackgroundColor(android.graphics.Color.WHITE))); onView(withId(R.id.webView)).perform(waitToBeDisplayed()); + onView(withId(R.id.webView)).perform(evaluateJavascript("onHcaptchaLoaded()")); final long waitToDisappearMs = 10000; onView(withId(R.id.loadingContainer)).perform(waitToDisappear(waitToDisappearMs)); } @@ -473,6 +480,39 @@ public void testTouchShouldNotCloseCaptchaWithoutDefaultLoadingIndicator() { } } + @Test + public void testInvisibleDialogDoesNotExecuteBeforeBridgeLoaded() throws InterruptedException { + final CountDownLatch loadedLatch = new CountDownLatch(1); + final CountDownLatch failureLatch = new CountDownLatch(1); + final HCaptchaStateListener listener = new HCaptchaStateTestAdapter() { + @Override + void onLoaded() { + loadedLatch.countDown(); + } + + @Override + void onFailure(HCaptchaException exception) { + failureLatch.countDown(); + } + }; + + try (FragmentScenario scenario = launch( + config.toBuilder() + .size(HCaptchaSize.INVISIBLE) + .hideDialog(false) + .build(), + internalConfig.toBuilder() + .htmlProvider(new HCaptchaTestHtml(false, true)) + .build(), + listener)) { + assertFalse(loadedLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); + assertFalse(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); + onView(withId(R.id.webView)).perform(evaluateJavascript("onHcaptchaLoaded()")); + assertTrue(loadedLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); + assertTrue(failureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); + } + } + @Test public void testBackShouldNotCloseCaptchaWithCustomRetry() { try (FragmentScenario scenario = launch( diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTest.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTest.java index 6c182af2..0e536f34 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTest.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTest.java @@ -1,5 +1,6 @@ package com.hcaptcha.sdk; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.hcaptcha.sdk.AssertUtil.waitHCaptchaWebViewToken; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -20,6 +21,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; public class HCaptchaTest { private static final long AWAIT_CALLBACK_MS = 5000; @@ -126,6 +128,41 @@ public void e2eWithDebugTokenHeadlessWebView() throws Exception { assertTrue(latch.await(E2E_AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); } + @Test + public void dialogVerifierReplaysLoadedAfterReset() throws Exception { + final CountDownLatch firstSuccessLatch = new CountDownLatch(1); + final CountDownLatch secondFailureLatch = new CountDownLatch(1); + final AtomicReference hCaptchaRef = new AtomicReference<>(); + + final HCaptchaInternalConfig resetAwareInternalConfig = internalConfig.toBuilder() + .htmlProvider(new HCaptchaTestHtml(true, false, true)) + .build(); + final HCaptchaConfig dialogConfig = config.toBuilder().hideDialog(false).build(); + + final ActivityScenario scenario = rule.getScenario(); + scenario.onActivity(activity -> { + final HCaptcha hCaptcha = HCaptcha.getClient(activity, resetAwareInternalConfig); + hCaptchaRef.set(hCaptcha); + hCaptcha.verifyWithHCaptcha(dialogConfig) + .addOnSuccessListener(response -> firstSuccessLatch.countDown()) + .addOnFailureListener(exception -> { + if (firstSuccessLatch.getCount() == 0 + && exception.getHCaptchaError() == HCaptchaError.ERROR) { + secondFailureLatch.countDown(); + } else { + fail("Unexpected failure: " + exception.getHCaptchaError()); + } + }); + }); + + waitHCaptchaWebViewToken(firstSuccessLatch, AWAIT_CALLBACK_MS); + getInstrumentation().waitForIdleSync(); + + scenario.onActivity(activity -> hCaptchaRef.get().verifyWithHCaptcha(dialogConfig)); + + assertTrue(secondFailureLatch.await(AWAIT_CALLBACK_MS, TimeUnit.MILLISECONDS)); + } + @Test(expected = IllegalStateException.class) public void badActivity() { Looper.prepare(); diff --git a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java index 70902e5e..d9948153 100644 --- a/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java +++ b/test/src/androidTest/java/com/hcaptcha/sdk/HCaptchaTestHtml.java @@ -5,13 +5,25 @@ class HCaptchaTestHtml implements IHCaptchaHtmlProvider { private final boolean callBridgeOnLoaded; + private final boolean failOnExecute; + private final boolean failOnExecuteAfterReset; HCaptchaTestHtml() { this(true); } HCaptchaTestHtml(boolean callBridgeOnLoaded) { + this(callBridgeOnLoaded, false); + } + + HCaptchaTestHtml(boolean callBridgeOnLoaded, boolean failOnExecute) { + this(callBridgeOnLoaded, failOnExecute, false); + } + + HCaptchaTestHtml(boolean callBridgeOnLoaded, boolean failOnExecute, boolean failOnExecuteAfterReset) { this.callBridgeOnLoaded = callBridgeOnLoaded; + this.failOnExecute = failOnExecute; + this.failOnExecuteAfterReset = failOnExecuteAfterReset; } @Override @@ -40,6 +52,7 @@ public String getHtml() { + " console.assert(typeof window.JSDI.getSysDebug() === 'object');\n" + " var BridgeObject = window.JSInterface;\n" + " var bridgeConfig = JSON.parse(BridgeObject.getConfig());\n" + + " var resetCount = 0;\n" + " function onHcaptchaLoaded() {\n" + " try {\n" + " BridgeObject.onLoaded();\n" @@ -64,9 +77,13 @@ public String getHtml() { + " TestObject.setData(JSON.stringify(arg));\n" + " }\n" + " function reset() {\n" + + " resetCount += 1;\n" + " document.getElementById(\"input-text\").value = \"reset\";\n" + " }\n" + " function execute() {\n" + + (failOnExecute ? " BridgeObject.onError(29);\n" : "") + + (failOnExecuteAfterReset + ? " if (resetCount > 1) { BridgeObject.onError(29); }\n" : "") + " }\n" + (callBridgeOnLoaded ? "onHcaptchaLoaded();\n" : "") + " \n"