Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,25 @@ package com.iterable.integration.tests

import android.content.Intent
import android.util.Log
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
import androidx.test.runner.lifecycle.Stage
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.By
import com.iterable.iterableapi.IterableApi
import com.iterable.iterableapi.IterableEmbeddedMessage
import androidx.test.uiautomator.Until
import com.iterable.integration.tests.activities.EmbeddedMessageTestActivity
import com.iterable.iterableapi.IterableApi
import com.iterable.iterableapi.ui.embedded.IterableEmbeddedView
import com.iterable.iterableapi.ui.embedded.IterableEmbeddedViewType
import org.awaitility.Awaitility
import org.json.JSONObject
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.TimeUnit

@RunWith(AndroidJUnit4::class)
class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
Expand Down Expand Up @@ -78,28 +75,21 @@ class EmbeddedMessageIntegrationTest : BaseIntegrationTest() {
val mainIntent = Intent(InstrumentationRegistry.getInstrumentation().targetContext, MainActivity::class.java)
mainActivityScenario = ActivityScenario.launch(mainIntent)

// Wait for MainActivity to be ready
Awaitility.await()
.atMost(5, TimeUnit.SECONDS)
.pollInterval(500, TimeUnit.MILLISECONDS)
.until {
val state = mainActivityScenario.state
Log.d(TAG, "🔧 MainActivity state: $state")
state == Lifecycle.State.RESUMED
}

Log.d(TAG, "🔧 MainActivity is ready!")
Log.d(TAG, "🔧 MainActivity launched")

// Step 2: Click the "Embedded Messages" button to navigate to EmbeddedMessageTestActivity
Log.d(TAG, "🔧 Step 2: Clicking 'Embedded Messages' button...")
val embeddedButton = uiDevice.findObject(UiSelector().resourceId("com.iterable.integration.tests:id/btnEmbeddedMessages"))
if (embeddedButton.exists()) {
embeddedButton.click()
Log.d(TAG, "🔧 Clicked Embedded Messages button successfully")
} else {
Log.e(TAG, "❌ Embedded Messages button not found!")
Assert.fail("Embedded Messages button not found in MainActivity")
}
Log.d(TAG, "🔧 Step 2: Waiting for and clicking 'Embedded Messages' button...")

// Use UiDevice.wait() which is the proper way to wait for UI elements in UiAutomator
val embeddedButton = uiDevice.wait(
Until.findObject(By.res("com.iterable.integration.tests", "btnEmbeddedMessages")),
5000 // 5 second timeout
)

Assert.assertNotNull("Embedded Messages button should be found", embeddedButton)
embeddedButton.click()

Log.d(TAG, "🔧 Clicked Embedded Messages button successfully")

// Step 3: Wait for EmbeddedMessageTestActivity to load
Log.d(TAG, "🔧 Step 3: Waiting for EmbeddedMessageTestActivity to load...")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;

import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.intent.Intents.intended;
Expand All @@ -22,11 +21,8 @@
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasData;
import static org.hamcrest.CoreMatchers.allOf;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

@RunWith(AndroidJUnit4.class)
public class IterableActionRunnerTest {
Expand Down Expand Up @@ -57,9 +53,14 @@ public void testOpenUrlAction() throws Exception {

@Test
public void testUrlHandlingOverride() throws Exception {
IterableUrlHandler urlHandlerMock = mock(IterableUrlHandler.class);
when(urlHandlerMock.handleIterableURL(any(Uri.class), any(IterableActionContext.class))).thenReturn(true);
IterableTestUtils.initIterableApi(new IterableConfig.Builder().setUrlHandler(urlHandlerMock).build());
// Use a simple implementation instead of mock for API 36+ compatibility
IterableUrlHandler urlHandler = new IterableUrlHandler() {
@Override
public boolean handleIterableURL(Uri uri, IterableActionContext context) {
return true;
}
};
IterableTestUtils.initIterableApi(new IterableConfig.Builder().setUrlHandler(urlHandler).build());

JSONObject actionData = new JSONObject();
actionData.put("type", "openUrl");
Expand All @@ -73,17 +74,31 @@ public void testUrlHandlingOverride() throws Exception {

@Test
public void testCustomAction() throws Exception {
IterableCustomActionHandler customActionHandlerMock = mock(IterableCustomActionHandler.class);
IterableTestUtils.initIterableApi(new IterableConfig.Builder().setCustomActionHandler(customActionHandlerMock).build());
// Track if the custom action handler was called (for API 36+ compatibility)
final boolean[] handlerCalled = {false};
final IterableAction[] capturedAction = {null};
final IterableActionContext[] capturedContext = {null};

IterableCustomActionHandler customActionHandler = (action, actionContext) -> {
handlerCalled[0] = true;
capturedAction[0] = action;
capturedContext[0] = actionContext;
return false;
};

IterableTestUtils.initIterableApi(new IterableConfig.Builder().setCustomActionHandler(customActionHandler).build());

JSONObject actionData = new JSONObject();
actionData.put("type", "customActionName");
IterableAction action = IterableAction.from(actionData);
IterableActionRunner.executeAction(getApplicationContext(), action, IterableActionSource.PUSH);

ArgumentCaptor<IterableActionContext> contextCaptor = ArgumentCaptor.forClass(IterableActionContext.class);
verify(customActionHandlerMock).handleIterableCustomAction(eq(action), contextCaptor.capture());
assertEquals(IterableActionSource.PUSH, contextCaptor.getValue().source);
// Verify the handler was called with correct parameters
assertTrue("Custom action handler should have been called", handlerCalled[0]);
assertNotNull("Action should not be null", capturedAction[0]);
assertEquals("Action type should match", "customActionName", capturedAction[0].getType());
assertNotNull("Context should not be null", capturedContext[0]);
assertEquals("Source should be PUSH", IterableActionSource.PUSH, capturedContext[0].source);
IterableTestUtils.initIterableApi(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import android.os.AsyncTask;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
Expand All @@ -30,6 +32,12 @@
import static junit.framework.Assert.assertTrue;
import static org.junit.Assert.assertThat;

/**
* Tests for IterableRequestTask API responses.
*
* Note: Uses AsyncTask.SERIAL_EXECUTOR for deterministic execution on API 36+
* where default AsyncTask behavior changed.
*/
@RunWith(AndroidJUnit4.class)
@MediumTest
public class IterableApiResponseTest {
Expand All @@ -39,20 +47,26 @@ public class IterableApiResponseTest {
@Before
public void setUp() throws IOException {
server = new MockWebServer();
// Explicitly start the server to ensure it's ready
try {
server.start();
} catch (IllegalStateException e) {
// Server may already be started by url() call below, which is fine
}
server.start();
IterableApi.overrideURLEndpointPath(server.url("").toString());
createIterableApi();
}

@After
public void tearDown() throws IOException {
server.shutdown();
server = null;
// Don't null IterableApi.sharedInstance - causes NPE with in-flight AsyncTasks
if (server != null) {
try {
// Drain any pending responses to prevent test contamination
while (server.takeRequest(100, TimeUnit.MILLISECONDS) != null) {
// Consume and discard
}
server.shutdown();
} catch (Exception e) {
// Ignore cleanup errors
}
server = null;
}
}

private void stubAnyRequestReturningStatusCode(int statusCode, JSONObject data) {
Expand Down Expand Up @@ -84,10 +98,10 @@ public void onSuccess(@NonNull JSONObject data) {
signal.countDown();
}
}, null);
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onSuccess is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onSuccess is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand All @@ -103,10 +117,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand All @@ -122,10 +136,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand All @@ -141,10 +155,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}


Expand All @@ -162,10 +176,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand All @@ -181,16 +195,19 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
public void testResponseCode401AuthError() throws Exception {
final CountDownLatch signal = new CountDownLatch(1);

// JWT errors trigger async retry logic which can cause race conditions with test cleanup
// Stub multiple responses for retries, but expect immediate failure callback
stubAnyRequestReturningStatusCode(401, "{\"msg\":\"JWT Authorization header error\",\"code\":\"InvalidJwtPayload\"}");
stubAnyRequestReturningStatusCode(401, "{\"msg\":\"JWT Authorization header error\",\"code\":\"InvalidJwtPayload\"}");

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, new IterableHelper.FailureHandler() {
Expand All @@ -200,10 +217,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand Down Expand Up @@ -246,7 +263,7 @@ public void onSuccess(@NonNull JSONObject successData) {
}
});

new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);
server.takeRequest(5, TimeUnit.SECONDS);

// Await for the background tasks to complete
Expand All @@ -261,14 +278,14 @@ public void testMaxRetriesOnMultipleInvalidJwtPayloads() throws Exception {

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null);
IterableRequestTask task = new IterableRequestTask();
task.execute(request);
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

RecordedRequest request1 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request3 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request4 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request5 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request6 = server.takeRequest(5, TimeUnit.SECONDS);
RecordedRequest request6 = server.takeRequest(1, TimeUnit.SECONDS);
assertNull("Request should be null since retries hit the max of 5", request6);
}

Expand All @@ -280,7 +297,7 @@ public void testResponseCode500() throws Exception {

IterableApiRequest request = new IterableApiRequest("fake_key", "", new JSONObject(), IterableApiRequest.POST, null, null, null);
IterableRequestTask task = new IterableRequestTask();
task.execute(request);
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

RecordedRequest request1 = server.takeRequest(1, TimeUnit.SECONDS);
RecordedRequest request2 = server.takeRequest(5, TimeUnit.SECONDS);
Expand All @@ -300,10 +317,10 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(1, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(5, TimeUnit.SECONDS));
server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}

@Test
Expand All @@ -319,9 +336,9 @@ public void onFailure(@NonNull String reason, @Nullable JSONObject data) {
signal.countDown();
}
});
new IterableRequestTask().execute(request);
new IterableRequestTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, request);

server.takeRequest(1, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(1, TimeUnit.SECONDS));
server.takeRequest(5, TimeUnit.SECONDS);
assertTrue("onFailure is called", signal.await(10, TimeUnit.SECONDS));
}
}
Loading
Loading