Skip to content
21 changes: 18 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ android {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

testOptions {
unitTests {
includeAndroidResources = true
}
}

sourceSets {
test {
java.srcDirs += 'src/sharedTest/java'
}
androidTest {
java.srcDirs += 'src/sharedTest/java'
}
}
}

dependencies {
Expand All @@ -44,17 +59,17 @@ dependencies {
// HTTP Server for syncing
implementation 'org.nanohttpd:nanohttpd:2.3.1'

testImplementation 'org.mockito:mockito-core:5.22.0'
testImplementation 'org.hamcrest:hamcrest-library:3.0'
testImplementation 'org.robolectric:robolectric:4.14.1'

// Testing dependencies (Updated for Android 16/API 36)
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test:rules:1.7.0'
androidTestImplementation 'androidx.test:runner:1.7.0'
androidTestImplementation 'androidx.test:core:1.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.7.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
androidTestImplementation 'org.mockito:mockito-android:5.22.0'
}

/**
Expand Down Expand Up @@ -112,4 +127,4 @@ tasks.named("preBuild") {
// Add this at the bottom of the file
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << "-Xlint:deprecation"
}
}
194 changes: 194 additions & 0 deletions app/src/androidTest/java/org/sil/hearthis/BookSelectionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package org.sil.hearthis;

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.matcher.IntentMatchers.hasComponent;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import android.view.View;

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

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import Script.BookInfo;
import Script.FileSystem;
import Script.RealScriptProvider;
import Script.TestFileSystem;

/**
* Tests the navigation flow from ChooseBookActivity to ChooseChapterActivity.
*/
@RunWith(AndroidJUnit4.class)
public class BookSelectionTest {

private TestFileSystem fakeFileSystem;

@Before
public void setUp() {
Intents.init();
ServiceLocator.theOneInstance = new ServiceLocator();

fakeFileSystem = new TestFileSystem();
fakeFileSystem.externalFilesDirectory = "root";
fakeFileSystem.project = "testProject";

// Default setup for most tests (partially complete books)
setupTestFileSystem("Matthew;10:0\nMark;8:0\n");
}

// Helper to allow different file setups
private void setupTestFileSystem(String infoTxtContent) {
StringBuilder infoTxtBuilder = new StringBuilder();
for (int i = 0; i < 39; i++) infoTxtBuilder.append("Book").append(i).append(";\n");
infoTxtBuilder.append(infoTxtContent);

fakeFileSystem.simulateFile(fakeFileSystem.getInfoTxtPath(), infoTxtBuilder.toString());
fakeFileSystem.SimulateDirectory(fakeFileSystem.getProjectDirectory());

ServiceLocator.getServiceLocator().externalFilesDirectory = fakeFileSystem.externalFilesDirectory;
ServiceLocator.getServiceLocator().setFileSystem(new FileSystem(fakeFileSystem));
ServiceLocator.getServiceLocator().setScriptProvider(new RealScriptProvider(fakeFileSystem.getProjectDirectory()));
}

@After
public void tearDown() {
Intents.release();
}

@Test
public void chooseBookActivity_displaysBooks() {
try (ActivityScenario<ChooseBookActivity> ignored = ActivityScenario.launch(ChooseBookActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

onView(withBookName("Matthew")).check(matches(isDisplayed()));
onView(withBookName("Mark")).check(matches(isDisplayed()));
}
}

@Test
public void selectingBook_navigatesToChapters() {
try (ActivityScenario<ChooseBookActivity> ignored = ActivityScenario.launch(ChooseBookActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

onView(withBookName("Matthew")).perform(click());

intended(allOf(
hasComponent(ChooseChapterActivity.class.getName()),
hasExtra(is("bookInfo"), instanceOf(BookInfo.class))
));

onView(withId(R.id.bookNameText)).check(matches(withText("Matthew")));
}
}

@Test
public void selectingChapter_navigatesToRecordActivity() {
try (ActivityScenario<ChooseBookActivity> ignored = ActivityScenario.launch(ChooseBookActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

onView(withBookName("Matthew")).perform(click());

InstrumentationRegistry.getInstrumentation().waitForIdleSync();

onView(withChapterNumber(1)).perform(click());

intended(allOf(
hasComponent(RecordActivity.class.getName()),
hasExtra("chapter", 1)
));
}
}

@Test
public void chooseBookActivity_showsRecordedStatus() {
// New setup for this specific test where Matthew is fully recorded
setupTestFileSystem("Matthew;10:10\nMark;8:0\n");

try (ActivityScenario<ChooseBookActivity> ignored = ActivityScenario.launch(ChooseBookActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

// Verify Matthew is marked as recorded, and Mark is not.
onView(withBookName("Matthew")).check(matches(isFullyRecorded()));
onView(withBookName("Mark")).check(matches(not(isFullyRecorded())));
}
}

/**
* Custom matcher to find a BookButton based on its Model's name.
*/
public static Matcher<View> withBookName(final String bookName) {
return new TypeSafeMatcher<>() {
@Override
public boolean matchesSafely(View view) {
if (view instanceof BookButton button) {
return button.Model != null && bookName.equals(button.Model.Name);
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("with book name: " + bookName);
}
};
}

/**
* Custom matcher to find a ChapterButton based on its chapter number.
*/
public static Matcher<View> withChapterNumber(final int chapterNumber) {
return new TypeSafeMatcher<>() {
@Override
public boolean matchesSafely(View view) {
if (view instanceof ChapterButton button) {
return button.chapterNumber == chapterNumber;
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("with chapter number: " + chapterNumber);
}
};
}

/**
* Custom matcher to check if a ProgressButton is fully recorded.
*/
public static Matcher<View> isFullyRecorded() {
return new TypeSafeMatcher<>() {
@Override
public boolean matchesSafely(View view) {
if (view instanceof ProgressButton button) {
return button.isAllRecorded();
}
return false;
}

@Override
public void describeTo(Description description) {
description.appendText("is fully recorded");
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public void createMainActivity_withScripture_NoSavedLocation_startsChooseBook()
TestFileSystem fakeFileSystem = new TestFileSystem();
fakeFileSystem.project = "kal";
String infoPath = fakeFileSystem.getInfoTxtPath();
fakeFileSystem.SimulateFile(infoPath, fakeFileSystem.getDefaultInfoTxtContent());
fakeFileSystem.simulateFile(infoPath, fakeFileSystem.getDefaultInfoTxtContent());
fakeFileSystem.SimulateDirectory(fakeFileSystem.getProjectDirectory());

final ServiceLocator serviceLocator = ServiceLocator.getServiceLocator();
Expand Down Expand Up @@ -82,4 +82,4 @@ public void createMainActivity_withScripture_NoSavedLocation_startsChooseBook()
serviceLocator.getScriptProvider().getRecordingFilePath(39, 1, 2));
}
}
}
}
102 changes: 102 additions & 0 deletions app/src/androidTest/java/org/sil/hearthis/ProjectSelectionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.sil.hearthis;

import static androidx.test.espresso.Espresso.onData;
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.matcher.IntentMatchers.hasComponent;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;

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

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import Script.FileSystem;
import Script.TestFileSystem;

/**
* Tests the ChooseProjectActivity logic for listing and selecting projects.
*/
@RunWith(AndroidJUnit4.class)
public class ProjectSelectionTest {

private TestFileSystem fakeFileSystem;

@Before
public void setUp() {
Intents.init();
ServiceLocator.theOneInstance = new ServiceLocator();

fakeFileSystem = new TestFileSystem();
fakeFileSystem.externalFilesDirectory = "root";
ServiceLocator.getServiceLocator().externalFilesDirectory = fakeFileSystem.externalFilesDirectory;
ServiceLocator.getServiceLocator().setFileSystem(new FileSystem(fakeFileSystem));
}

@After
public void tearDown() {
Intents.release();
}

@Test
public void chooseProjectActivity_listsAvailableProjects() {
// Setup multiple project directories
fakeFileSystem.SimulateDirectory("root/ProjectA");
fakeFileSystem.SimulateDirectory("root/ProjectB");

try (ActivityScenario<ChooseProjectActivity> ignored = ActivityScenario.launch(ChooseProjectActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

// Verify both project names appear in the list
onView(withText("ProjectA")).check(matches(isDisplayed()));
onView(withText("ProjectB")).check(matches(isDisplayed()));
}
}

@Test
public void selectingProject_navigatesToChooseBook() {
// Setup one project with a valid info.txt so it can load books
fakeFileSystem.SimulateDirectory("root/ProjectA");
fakeFileSystem.simulateFile("root/ProjectA/info.txt", "Matthew;10:0\n");

try (ActivityScenario<ChooseProjectActivity> ignored = ActivityScenario.launch(ChooseProjectActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

// Click on ProjectA in the list
onData(allOf(is(instanceOf(String.class)), is("ProjectA"))).perform(click());

// Verify navigation to ChooseBookActivity (since there's no saved location)
intended(hasComponent(ChooseBookActivity.class.getName()));
}
}

@Test
public void selectingProject_withSavedLocation_navigatesToRecord() {
// Setup a project with a saved location in status.txt
fakeFileSystem.SimulateDirectory("root/ProjectA");
fakeFileSystem.simulateFile("root/ProjectA/info.txt", "Matthew;10:0\n");
// Status: Book 0, Chapter 0, Line 5
fakeFileSystem.simulateFile("root/ProjectA/status.txt", "0;0;5");

try (ActivityScenario<ChooseProjectActivity> ignored = ActivityScenario.launch(ChooseProjectActivity.class)) {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();

// Click on ProjectA
onData(allOf(is(instanceOf(String.class)), is("ProjectA"))).perform(click());

// Verify navigation directly to RecordActivity
intended(hasComponent(RecordActivity.class.getName()));
}
}
}
Loading