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
2 changes: 1 addition & 1 deletion 5calls/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ android {
targetSdkVersion 36
versionCode 86
versionName '2.4.5'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
testInstrumentationRunner "org.a5calls.android.a5calls.LocaleAwareTestRunner"
signingConfig signingConfigs.debug
}
buildTypes {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.a5calls.android.a5calls;

import static androidx.test.espresso.matcher.ViewMatchers.isClickable;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static org.hamcrest.Matchers.allOf;
import static org.junit.Assert.assertNotNull;

import android.content.Context;
import android.os.SystemClock;
import android.view.View;

import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import java.util.Locale;

import com.android.volley.toolbox.BasicNetwork;
import com.google.android.material.appbar.CollapsingToolbarLayout;

import org.a5calls.android.a5calls.model.AccountManager;
import org.a5calls.android.a5calls.net.FakeRequestQueue;
import org.a5calls.android.a5calls.net.FiveCallsApi;
import org.a5calls.android.a5calls.net.MockHttpStack;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;

/**
* Base class for all instrumentation tests in the app.
* Handles common setup for mocking network requests and accessing context.
*/
@RunWith(AndroidJUnit4.class)
public abstract class BaseIntegrationTest {

protected Context mContext;
protected MockHttpStack mHttpStack;
protected FakeRequestQueue mRequestQueue;
protected FiveCallsApi mApi;
protected Locale mLocale;

@Before
public void setUp() {
mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
mLocale = Locale.getDefault();

// Use a fixed caller ID for consistent test results
AccountManager.Instance.setCallerID(mContext, "itMe");

mHttpStack = new MockHttpStack();
BasicNetwork basicNetwork = new BasicNetwork(mHttpStack);
mRequestQueue = new FakeRequestQueue(basicNetwork);

mApi = new FiveCallsApi("itMe", mRequestQueue, mContext);
}

@After
public void tearDown() {
if (mRequestQueue != null) {
mRequestQueue.mRequest = null;
}
}

/**
* Helper to wait for asynchronous network requests to complete in tests.
*/
protected void waitForHttpRequestComplete() {
assertNotNull(mRequestQueue.mRequest);
mRequestQueue.start();

// Wait for the async stuff.
// TODO: Use a more robust synchronization mechanism like IdlingResource
SystemClock.sleep(200);
}

/**
* Custom matcher to check if a RecyclerView has exactly one item
*/
public static Matcher<View> hasExactlyOneItem() {
return new TypeSafeMatcher<>() {
@Override
protected boolean matchesSafely(View view) {
if (!(view instanceof RecyclerView recyclerView)) {
return false;
}
return recyclerView.getAdapter() != null && recyclerView.getAdapter().getItemCount() == 1;
}

@Override
public void describeTo(Description description) {
description.appendText("RecyclerView with exactly one item");
}
};
}

// Custom matcher that matches only the first view matching the given matcher.
public static Matcher<View> first(final Matcher<View> matcher) {
return new TypeSafeMatcher<>() {
boolean matched = false;

@Override
public boolean matchesSafely(View view) {
if (matched) {
return false;
}
if (matcher.matches(view)) {
matched = true;
return true;
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("first view matching: ");
matcher.describeTo(description);
}
};
}

// Custom matcher to check if a CollapsingToolbarLayout's title contains specific text
public static Matcher<View> withCollapsingToolbarTitle(final Matcher<String> textMatcher) {
return new TypeSafeMatcher<>() {
@Override
public boolean matchesSafely(View view) {
if (!(view instanceof CollapsingToolbarLayout toolbarLayout)) {
return false;
}
CharSequence title = toolbarLayout.getTitle();
return title != null && textMatcher.matches(title.toString());
}

@Override
public void describeTo(Description description) {
description.appendText("with toolbar title: ");
textMatcher.describeTo(description);
}
};
}

/**
* A custom click action that only requires the view to be displayed,
* bypassing the 90% visibility constraint.
*/
public static ViewAction clickVisible() {
return new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return allOf(isDisplayed(), isClickable());
}

@Override
public String getDescription() {
return "click visible view";
}

@Override
public void perform(UiController uiController, View view) {
view.performClick();
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package org.a5calls.android.a5calls

import android.content.Context
import android.os.Build
import android.os.Bundle
import android.os.LocaleList
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.test.runner.AndroidJUnitRunner
import java.util.Locale

class LocaleAwareTestRunner : AndroidJUnitRunner() {
private var mArguments: Bundle? = null

override fun onCreate(arguments: Bundle) {
mArguments = arguments
val localeTag = arguments.getString("locale")
System.err.println("LocaleAwareTestRunner intercepted localeTag: $localeTag")

if (!localeTag.isNullOrEmpty()) {
val locale = Locale.forLanguageTag(localeTag)
setGlobalLocale(locale)
}
super.onCreate(arguments)
}

override fun onStart() {
// Use AppCompatDelegate to set locales globally for the app.
// This is the most reliable way for AppCompat-based activities.
val localeTag = if (mArguments != null) mArguments!!.getString("locale") else null
runOnMainSync {
if (!localeTag.isNullOrEmpty()) {
System.err.println("LocaleAwareTestRunner setting AppCompatDelegate locales to: $localeTag")
AppCompatDelegate.setApplicationLocales(
LocaleListCompat.forLanguageTags(
localeTag
)
)
} else {
// Reset to system default if no locale specified to avoid persistence from previous runs
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())
}
}
super.onStart()
}

@Suppress("deprecation")
private fun setGlobalLocale(locale: Locale) {
System.err.println("LocaleAwareTestRunner Setting global locale to $locale")
Locale.setDefault(locale)

// Update configuration for both target context and application context
updateContextLocale(targetContext, locale)
updateContextLocale(targetContext.applicationContext, locale)
}

@Suppress("deprecation")
private fun updateContextLocale(context: Context?, locale: Locale) {
if (context == null) return

val resources = context.resources
val configuration = resources.configuration

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
configuration.setLocales(LocaleList(locale))
} else {
configuration.locale = locale
}

resources.updateConfiguration(configuration, resources.displayMetrics)
}
}
Original file line number Diff line number Diff line change
@@ -1,65 +1,53 @@
package org.a5calls.android.a5calls.controller;

import android.content.Context;

import androidx.test.core.app.ActivityScenario;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import com.android.volley.RequestQueue;
import com.android.volley.toolbox.BasicNetwork;

import org.a5calls.android.a5calls.AppSingleton;
import org.a5calls.android.a5calls.BaseIntegrationTest;
import org.a5calls.android.a5calls.model.AccountManager;
import org.a5calls.android.a5calls.net.FakeRequestQueue;
import org.a5calls.android.a5calls.net.FiveCallsApi;
import org.a5calls.android.a5calls.net.MockHttpStack;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;

/**
* Base class for MainActivity integration tests that contains shared setup and utility methods.
*/
@RunWith(AndroidJUnit4.class)
public abstract class MainActivityBaseTest {
public abstract class MainActivityBaseTest extends BaseIntegrationTest {

protected MockHttpStack mHttpStack;
protected RequestQueue mOriginalRequestQueue;
protected FiveCallsApi mOriginalApi;
protected String mOriginalAddress;
protected ActivityScenario<MainActivity> scenario;

@Before
public void setUp() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
super.setUp();
// Save original state
mOriginalRequestQueue = AppSingleton.getInstance(context).getRequestQueue();
mOriginalApi = AppSingleton.getInstance(context).getJsonController();
mOriginalRequestQueue = AppSingleton.getInstance(mContext).getRequestQueue();
mOriginalApi = AppSingleton.getInstance(mContext).getJsonController();

// Save original location
mOriginalAddress = AccountManager.Instance.getAddress(context);
mOriginalAddress = AccountManager.Instance.getAddress(mContext);

// Set a mock location to avoid location prompts
AccountManager.Instance.setAddress(context, "90210");
AccountManager.Instance.setAddress(mContext, "90210");

// Mark tutorial as seen to bypass onboarding screen
AccountManager.Instance.setTutorialSeen(context, true);

// Create mock HTTP stack
mHttpStack = new MockHttpStack();
AccountManager.Instance.setTutorialSeen(mContext, true);
}


@After
public void tearDown() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Restore original state
AppSingleton.getInstance(context).setRequestQueue(mOriginalRequestQueue);
AppSingleton.getInstance(context).setFiveCallsApi(mOriginalApi);
AppSingleton.getInstance(mContext).setRequestQueue(mOriginalRequestQueue);
AppSingleton.getInstance(mContext).setFiveCallsApi(mOriginalApi);

// Restore original location
AccountManager.Instance.setAddress(context, mOriginalAddress);
AccountManager.Instance.setAddress(mContext, mOriginalAddress);

// Close the activity scenario if it's open
if (scenario != null) {
Expand All @@ -71,24 +59,22 @@ public void tearDown() {
* Sets up the mock request queue and API
*/
protected void setupMockRequestQueue() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
// Create a custom RequestQueue with our mock HTTP stack
BasicNetwork basicNetwork = new BasicNetwork(mHttpStack);
FakeRequestQueue requestQueue = new FakeRequestQueue(basicNetwork);
requestQueue.start();

// Replace the app's RequestQueue with our mock
AppSingleton.getInstance(context).setRequestQueue(requestQueue);
AppSingleton.getInstance(mContext).setRequestQueue(requestQueue);

// Create a new FiveCallsApi with our mock RequestQueue
String callerId = AccountManager.Instance.getCallerID(context);
FiveCallsApi api = new FiveCallsApi(callerId, requestQueue, context);
AppSingleton.getInstance(context).setFiveCallsApi(api);
String callerId = AccountManager.Instance.getCallerID(mContext);
FiveCallsApi api = new FiveCallsApi(callerId, requestQueue, mContext);
AppSingleton.getInstance(mContext).setFiveCallsApi(api);
}

/**
* Launches the MainActivity and waits for it to load
*
* @param waitTimeMs time to wait for the activity to load
*/
protected void launchMainActivity(int waitTimeMs) {
Expand Down
Loading