diff --git a/app/src/androidTest/java/net/megaproxy487/DocumentsDeviceTest.kt b/app/src/androidTest/java/net/megaproxy487/DocumentsDeviceTest.kt index 1432403..9ee1c7c 100644 --- a/app/src/androidTest/java/net/megaproxy487/DocumentsDeviceTest.kt +++ b/app/src/androidTest/java/net/megaproxy487/DocumentsDeviceTest.kt @@ -5,7 +5,7 @@ import androidx.compose.material3.MaterialTheme import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.uiautomator.By import androidx.test.uiautomator.Until -import androidx.test.uiautomator.UiSelector +import android.view.accessibility.AccessibilityNodeInfo import net.megaproxy487.data.ConfigTransfer import net.megaproxy487.data.ConfigSubscription import kotlinx.coroutines.runBlocking @@ -36,9 +36,14 @@ class DocumentsDeviceTest : DeviceTestBase() { } private fun downloads() { systemNode(By.desc("Show roots")).click() - val downloads = device.findObject(UiSelector().resourceId("android:id/title").text("Downloads")) - assertTrue("Downloads root missing", downloads.waitForExists(10_000)) - downloads.click() + systemNode(By.res("android:id/title").text("Downloads")) + // Drawer animation changes coordinates after discovery. Invoke the row's action instead. + var node = requireNotNull(instrumentation.uiAutomation.rootInActiveWindow + ?.findAccessibilityNodeInfosByViewId("android:id/title") + ?.firstOrNull { it.text?.toString() == "Downloads" }) { "Downloads root missing" } + while (!node.isClickable) node = requireNotNull(node.parent) { "Downloads root is not actionable" } + assertTrue("Downloads root rejected click", node.performAction(AccessibilityNodeInfo.ACTION_CLICK)) + assertTrue("Document roots drawer did not close", device.wait(Until.gone(By.text("Open from")), 10_000)) } private fun importFile() { appNode(By.desc(text(R.string.import_action)).enabled(true)).click() diff --git a/scripts/android_device_tests.py b/scripts/android_device_tests.py index cd1ab7f..24f933b 100644 --- a/scripts/android_device_tests.py +++ b/scripts/android_device_tests.py @@ -71,8 +71,14 @@ def require_success(output): def finish_case(shell, results, label, case, suite, log_start): error_message = None + evidence = ( + (("anr", ("dumpsys", "dropbox", "--print", "data_app_anr")),) + if case.find("failure") is not None + else () + ) for name, words in ( ("logcat", ("logcat", "-d", "-v", "threadtime", "-T", log_start)), + *evidence, ("force-stop", ("am", "force-stop", PACKAGE)), ): try: @@ -84,7 +90,7 @@ def finish_case(shell, results, label, case, suite, log_start): message = f"{name} failed ({type(error).__name__}, exit {getattr(error, 'returncode', 'unknown')})" ET.SubElement(case, "failure", message=message) error_message = error_message or message - if name == "logcat" or error_message: + if name != "force-stop" or error_message: (results / (label + f"-{name}.txt")).write_text(output) suite.set("tests", str(len(suite))) suite.set("failures", str(sum(c.find("failure") is not None for c in suite))) diff --git a/scripts/tests/test_android_device_tests.py b/scripts/tests/test_android_device_tests.py index 08df016..b21a272 100644 --- a/scripts/tests/test_android_device_tests.py +++ b/scripts/tests/test_android_device_tests.py @@ -98,6 +98,26 @@ def test_logcat_failure_preserves_evidence_and_failed_report(self): (results / "test-force-stop.txt").read_text(), "device offline" ) + def test_failed_case_collects_anr_stacks_before_stopping_app(self): + suite = ET.Element("testsuite") + case = ET.SubElement(suite, "testcase", time="1.0") + ET.SubElement(case, "failure", message="Instrumentation timed out") + shell = Mock(side_effect=["logcat", "ANR thread stacks", ""]) + with tempfile.TemporaryDirectory() as directory: + results = Path(directory) + m.finish_case(shell, results, "test", case, suite, "10-22 08:18:00.000") + self.assertEqual( + (results / "test-anr.txt").read_text(), "ANR thread stacks" + ) + self.assertEqual( + shell.call_args_list[-2:], + [ + call("dumpsys", "dropbox", "--print", "data_app_anr"), + call("am", "force-stop", m.PACKAGE), + ], + ) + self.assertEqual(suite.get("failures"), "1") + def test_adb_success_is_not_enough(self): for output in ( "INSTRUMENTATION_CODE: 0",