Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fc65c21
Add automated tests for password-protected ODT files
TomTasche Jul 13, 2025
d2fa8f9
Fix UI test for password-protected documents
TomTasche Jul 13, 2025
031efce
Make password UI test more robust for CI environment
TomTasche Jul 20, 2025
b5eefdf
Add CI test artifact uploads and improve test debugging
TomTasche Jul 20, 2025
16de141
Simplify logcat capture in CI tests
TomTasche Jul 20, 2025
834b174
Add debugging for password test CI failure
TomTasche Jul 20, 2025
344bb15
Add enhanced debugging for password test CI failures
TomTasche Jul 26, 2025
dbb74eb
Revert "Add enhanced debugging for password test CI failures"
TomTasche Jul 26, 2025
95317e5
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Jul 28, 2025
5630e53
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
andiwand Jul 28, 2025
ff86414
Merge branch 'add-password-protected-tests' of github.com:opendocumen…
TomTasche Aug 3, 2025
7a4ae23
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Aug 3, 2025
9b185a2
stuff
TomTasche Aug 3, 2025
bdd1adf
make test always fail
TomTasche Aug 3, 2025
9a08c14
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Aug 15, 2025
0b359d2
cleanup
TomTasche Aug 15, 2025
4177524
Update app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java
TomTasche Aug 15, 2025
1897b41
raise version, fix CoreTest
TomTasche Aug 15, 2025
221bc31
Merge branch 'add-password-protected-tests' of github.com:opendocumen…
TomTasche Aug 15, 2025
8317516
undo gradle upgrade (breaks fastlane)
TomTasche Aug 15, 2025
9b6abe2
fix edit test
TomTasche Sep 6, 2025
725ccb4
REVERTME: add test script
TomTasche Sep 21, 2025
f4753f1
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Sep 21, 2025
f105cba
Revert "REVERTME: add test script"
TomTasche Sep 22, 2025
8312eb3
print edit errors
TomTasche Sep 22, 2025
a7492e9
Merge branch 'main' into add-password-protected-tests
andiwand Sep 22, 2025
4e948b5
Merge branch 'main' of github.com:opendocument-app/OpenDocument.droid…
TomTasche Sep 22, 2025
79f24bd
fix test
TomTasche Sep 22, 2025
5bcc2f6
Merge branch 'add-password-protected-tests' of github.com:opendocumen…
TomTasche Sep 22, 2025
65d9a7f
Merge branch 'main' into add-password-protected-tests
andiwand Sep 23, 2025
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
41 changes: 40 additions & 1 deletion .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,43 @@ jobs:
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disable-animations: true
script: ./gradlew connectedCheck
script: |
# Clear logcat before tests
adb logcat -c

# Run tests
./gradlew connectedCheck

# Dump logcat after tests
adb logcat -d > logcat.txt

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.api-level }}
path: |
app/build/reports/androidTests/
app/build/outputs/androidTest-results/
if-no-files-found: warn

- name: Upload test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: test-logs-${{ matrix.api-level }}
path: |
app/build/outputs/logs/
app/build/test-results/
if-no-files-found: warn

- name: Upload emulator logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: emulator-logs-${{ matrix.api-level }}
path: |
~/.android/avd/*.avd/config.ini
~/.android/avd/*.avd/*.log
logcat.txt
if-no-files-found: warn
Binary file added app/src/androidTest/assets/password-test.odt
Binary file not shown.
62 changes: 60 additions & 2 deletions app/src/androidTest/java/at/tomtasche/reader/test/CoreTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
@RunWith(AndroidJUnit4.class)
public class CoreTest {
private File m_testFile;
private File m_passwordTestFile;

@Before
public void initializeCore() {
Expand All @@ -36,19 +37,26 @@ public void initializeCore() {
public void extractTestFile() throws IOException {
Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
m_testFile = new File(appCtx.getCacheDir(), "test.odt");
m_passwordTestFile = new File(appCtx.getCacheDir(), "password-test.odt");

Context testCtx = InstrumentationRegistry.getInstrumentation().getContext();
AssetManager assetManager = testCtx.getAssets();
try (InputStream inputStream = assetManager.open("test.odt")) {
copy(inputStream, m_testFile);
}
try (InputStream inputStream = assetManager.open("password-test.odt")) {
copy(inputStream, m_passwordTestFile);
}
}

@After
public void cleanupTestFile() {
if (null != m_testFile) {
m_testFile.delete();
}
if (null != m_passwordTestFile) {
m_passwordTestFile.delete();
}
}

private static void copy(InputStream src, File dst) throws IOException {
Expand All @@ -70,18 +78,68 @@ public void test() {
CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_testFile.getAbsolutePath();
coreOptions.outputPath = outputPath.getPath();
coreOptions.cachePath = cachePath.getPath();
coreOptions.editable = true;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(0, coreResult.errorCode);

File resultFile = new File(cacheDir, "result");
coreOptions.outputPath = resultFile.getPath();

String htmlDiff = "{\"modifiedText\":{\"3\":\"This is a simple test document to demonstrate the DocumentLoadewwwwr example!\"}}";
String htmlDiff = "{\"modifiedText\":{\"/child:1/child:0\":\"This is a simple testoooo document to demonstrate the DocumentLoader example!\",\"/child:3/child:0\":\"This is a simple testaaaa document to demonstrate the DocumentLoader example!\"}}";

CoreWrapper.CoreResult result = CoreWrapper.backtranslate(coreOptions, htmlDiff);
Assert.assertEquals(0, result.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithoutPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(-2, coreResult.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithWrongPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.password = "wrongpassword";
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(-2, coreResult.errorCode);
}

@Test
public void testPasswordProtectedDocumentWithCorrectPassword() {
File cacheDir = InstrumentationRegistry.getInstrumentation().getTargetContext().getCacheDir();
File outputDir = new File(cacheDir, "output_password_test");
File cachePath = new File(cacheDir, "core_cache");

CoreWrapper.CoreOptions coreOptions = new CoreWrapper.CoreOptions();
coreOptions.inputPath = m_passwordTestFile.getAbsolutePath();
coreOptions.outputPath = outputDir.getPath();
coreOptions.password = "passwort";
coreOptions.editable = false;
coreOptions.cachePath = cachePath.getPath();

CoreWrapper.CoreResult coreResult = CoreWrapper.parse(coreOptions);
Assert.assertEquals(0, coreResult.errorCode);
}
}
113 changes: 103 additions & 10 deletions app/src/androidTest/java/at/tomtasche/reader/test/MainActivityTests.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package at.tomtasche.reader.test;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.clearText;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.intent.matcher.IntentMatchers.hasAction;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
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.anyOf;
import static org.hamcrest.Matchers.equalTo;

import android.app.Activity;
import android.app.Instrumentation;
Expand All @@ -18,6 +23,7 @@
import android.content.res.AssetManager;
import android.net.Uri;
import android.util.ArrayMap;
import android.util.Log;

import androidx.core.content.FileProvider;
import androidx.test.espresso.IdlingRegistry;
Expand Down Expand Up @@ -55,12 +61,14 @@ public class MainActivityTests {

// Yes, this is ActivityTestRule instead of ActivityScenario, because ActivityScenario does not actually work.
// Issue ID may or may not be added later.
// Launch activity manually to ensure complete restart between tests
@Rule
public ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class);
public ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class, false, false);

@Before
public void setUp() {
MainActivity mainActivity = mainActivityActivityTestRule.getActivity();
// Launch a fresh activity for each test
MainActivity mainActivity = mainActivityActivityTestRule.launchActivity(null);

m_idlingResource = mainActivity.getOpenFileIdlingResource();
IdlingRegistry.getInstance().register(m_idlingResource);
Expand All @@ -70,15 +78,29 @@ public void setUp() {
mainActivity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));

Intents.init();

// Log test setup for debugging
Log.d("MainActivityTests", "setUp() called for test: " + getClass().getName());
}

@After
public void tearDown() {
Log.d("MainActivityTests", "tearDown() called");

Intents.release();

if (null != m_idlingResource) {
IdlingRegistry.getInstance().unregister(m_idlingResource);
}

// Finish and wait for activity to be destroyed
MainActivity activity = mainActivityActivityTestRule.getActivity();
if (activity != null) {
mainActivityActivityTestRule.finishActivity();

// Use Instrumentation to wait until activity is destroyed
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
}

private static void copy(InputStream src, File dst) throws IOException {
Expand All @@ -104,7 +126,7 @@ public static void extractTestFiles() throws IOException {

AssetManager testAssetManager = instrumentation.getContext().getAssets();

for (String filename: new String[] {"test.odt", "dummy.pdf"}) {
for (String filename: new String[] {"test.odt", "dummy.pdf", "password-test.odt"}) {
File targetFile = new File(testDocumentsDir, filename);
try (InputStream inputStream = testAssetManager.open(filename)) {
copy(inputStream, targetFile);
Expand Down Expand Up @@ -135,21 +157,21 @@ public void testODT() {
);

onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed()))
.perform(click());
.perform(click());

// The menu item could be either Documents or Files.
onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed()))
.perform(click());

// next onView will be blocked until m_idlingResource is idle.
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled()))
.withFailureHandler((error, viewMatcher) -> {
// fails on small screens, try again with overflow menu
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());
.withFailureHandler((error, viewMatcher) -> {
// fails on small screens, try again with overflow menu
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());

onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
}

@Test
Expand Down Expand Up @@ -183,5 +205,76 @@ public void testPDF() {
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});

try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

@Test
public void testPasswordProtectedODT() {
File testFile = s_testFiles.get("password-test.odt");
Assert.assertNotNull(testFile);

// Check if the file exists and is readable
Assert.assertTrue("Password test file does not exist: " + testFile.getAbsolutePath(), testFile.exists());
Assert.assertTrue("Password test file is not readable: " + testFile.getAbsolutePath(), testFile.canRead());

// Log file info for debugging CI issues
Log.d("MainActivityTests", "Password test file path: " + testFile.getAbsolutePath());
Log.d("MainActivityTests", "Password test file size: " + testFile.length());
Log.d("MainActivityTests", "All test files: " + s_testFiles.keySet());

// Double-check we're using the right file
Assert.assertEquals("password-test.odt file size mismatch", 12671L, testFile.length());

Context appCtx = InstrumentationRegistry.getInstrumentation().getTargetContext();
Uri testFileUri = FileProvider.getUriForFile(appCtx, appCtx.getPackageName() + ".provider", testFile);
Intents.intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)).respondWith(
new Instrumentation.ActivityResult(Activity.RESULT_OK,
new Intent()
.setData(testFileUri)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
)
);

onView(allOf(withId(R.id.menu_open), withContentDescription("Open document"), isDisplayed()))
.perform(click());

onView(allOf(withId(android.R.id.text1), anyOf(withText("Documents"), withText("Files")), isDisplayed()))
.perform(click());

// Wait for the password dialog to appear
onView(withText("This document is password-protected"))
.check(matches(isDisplayed()));

// Enter wrong password first
onView(withClassName(equalTo("android.widget.EditText")))
.perform(typeText("wrongpassword"));

onView(withId(android.R.id.button1))
.perform(click());

// Should show password dialog again for wrong password
onView(withText("This document is password-protected"))
.check(matches(isDisplayed()));

// Clear the text field and enter correct password
onView(withClassName(equalTo("android.widget.EditText")))
.perform(clearText(), typeText("passwort"));

onView(withId(android.R.id.button1))
.perform(click());

// Check if edit button becomes available (indicating successful load)
onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isEnabled()))
.withFailureHandler((error, viewMatcher) -> {
onView(allOf(withContentDescription("More options"), isDisplayed())).perform(click());

onView(allOf(withId(R.id.menu_edit), withContentDescription("Edit document"), isDisplayed()))
.perform(click());
});
}
}
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
android:versionCode="192"
android:versionName="3.38"
android:versionCode="195"
android:versionName="3.40"
tools:ignore="GoogleAppIndexingWarning">

<uses-permission android:name="android.permission.INTERNET" />
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/cpp/core_wrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,13 +269,22 @@ Java_at_tomtasche_reader_background_CoreWrapper_backtranslateNative(JNIEnv *env,
jfieldID outputPathField = env->GetFieldID(resultClass, "outputPath", "Ljava/lang/String;");
env->SetObjectField(result, outputPathField, outputPath);

__android_log_print(ANDROID_LOG_DEBUG, "smn", "HTML diff: %s", htmlDiffC);

try {
odr::html::edit(*s_document, htmlDiffC);

env->ReleaseStringUTFChars(htmlDiff, htmlDiffC);
} catch (const std::exception &e) {
env->ReleaseStringUTFChars(htmlDiff, htmlDiffC);

__android_log_print(ANDROID_LOG_ERROR, "smn", "Failed to edit document: %s", e.what());
env->SetIntField(result, errorField, -6);
return result;
} catch (...) {
env->ReleaseStringUTFChars(htmlDiff, htmlDiffC);

__android_log_print(ANDROID_LOG_ERROR, "smn", "Failed to edit document: unknown exception");
env->SetIntField(result, errorField, -6);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public static void initialize(Context context) {
public static class CoreOptions {
public boolean ooxml;
public boolean txt;
// TODO: remove
public boolean pdf;

public boolean editable;
Expand Down
Loading