From 0a50e5d348ca956dd77a9b40b4aaa7bd46e91465 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:10:23 +0000 Subject: [PATCH] ui tests: known issues applied by an outer JUnit rule (teardown crashes); finish the IME composing word before Enter; field text in typing failures; flake 15.4.1e --- .../simplex/app/ui/android/AndroidHelpers.kt | 3 ++ .../ui/android/ComposeKeyboardAndroidTest.kt | 2 +- .../chat/simplex/app/ui/harness/UiTest.kt | 24 +++++++-------- .../chat/simplex/app/ui/harness/UiTest.kt | 30 +++++++++---------- .../chat/simplex/app/ui/harness/AppCrashes.kt | 22 +++++++------- .../2026-10-10-desktop-ui-test-dsl-plan.md | 4 +++ 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/AndroidHelpers.kt b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/AndroidHelpers.kt index ccf0047fa5..d8073099d3 100644 --- a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/AndroidHelpers.kt +++ b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/AndroidHelpers.kt @@ -127,7 +127,10 @@ private class ImeEnterKey : ViewAction { check((info.inputType and InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0 && (info.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { "the soft keyboard's Enter is an editor action for the message field: inputType 0x${info.inputType.toString(16)}, imeOptions 0x${info.imeOptions.toString(16)}" } + ic.beginBatchEdit() + ic.finishComposingText() ic.commitText("\n", 1) + ic.endBatchEdit() uiController.loopMainThreadUntilIdle() } } diff --git a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/ComposeKeyboardAndroidTest.kt b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/ComposeKeyboardAndroidTest.kt index 1d0159061e..c33f6b673c 100644 --- a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/ComposeKeyboardAndroidTest.kt +++ b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/android/ComposeKeyboardAndroidTest.kt @@ -14,7 +14,7 @@ class ComposeKeyboardAndroidTest : UiTest() { val chat = alice.chatList.open("bob") typeInto(Ui.Chat.Message, "hello") pressImeEnter() - poll(what = "a line break after `hello` in the message field") { editableText(Ui.Chat.Message) == "hello\n" } + poll(what = "a line break after `hello` in the message field") { editableText(Ui.Chat.Message).let { check(it == "hello\n") { "the field holds `$it`" }; true } } check(chat.itemTexts().none { "hello" in it }) { "Enter sent the message: ${chat.itemTexts()}" } chat.send("there") bob.expectsMessage("alice> hello", CONNECT_TIMEOUT_MS) diff --git a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/harness/UiTest.kt b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/harness/UiTest.kt index c991a05b98..1e8f8d3d82 100644 --- a/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/harness/UiTest.kt +++ b/apps/multiplatform/android/src/androidTest/java/chat/simplex/app/ui/harness/UiTest.kt @@ -28,8 +28,8 @@ import chat.simplex.common.helpers.APPLICATION_ID import java.io.File abstract class UiTest { - @get:Rule val rule: ComposeTestRule = createEmptyComposeRule() - @get:Rule val knownIssue = KnownIssueRule() + @get:Rule(order = 0) val knownIssue = KnownIssueRule() + @get:Rule(order = 1) val rule: ComposeTestRule = createEmptyComposeRule() fun uiTest(body: UiScope.() -> Unit) { val watchdog = Watchdog.start(HARD_TIMEOUT_MS) @@ -37,16 +37,14 @@ abstract class UiTest { UiTestApp.bootOnce() AppCrashes.install() AppCrashes.clear() - withKnownIssue(knownIssue.issues) { - val scope = UiScope(rule) - try { - scope.body() - scope.checkEnd() - } finally { - scope.finish() - } - AppCrashes.failIfAny() + val scope = UiScope(rule) + try { + scope.body() + scope.checkEnd() + } finally { + scope.finish() } + AppCrashes.failIfAny() } finally { watchdog.cancel() } @@ -188,7 +186,7 @@ class UiScope(val rule: ComposeTestRule) { rule.mainClock.advanceTimeBy(48, ignoreFrameDuration = true) try { rule.waitForIdle() - } catch (e: RuntimeException) { + } catch (e: Exception) { if (e.javaClass.simpleName != "ComposeNotIdleException") throw e val shot = if (screenshot) FailureShots.take("UI not idle while waiting for $what") else null throw AssertionError("the app's UI did not become idle while waiting for $what; in flight: ${UiIdle.inFlight().joinToString { it.what }}; screenshot: $shot", e) @@ -323,7 +321,7 @@ class UiScope(val rule: ComposeTestRule) { waitStable(key) settle() messageEditText(key).perform(CommitText(value)) - poll(what = "the message field to hold `$value`") { editableText(key) == value } + poll(what = "the message field to hold `$value`") { editableText(key).let { check(it == value) { "the field holds `$it`" }; true } } } fun enterText(key: UiKey, text: String) { diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ui/harness/UiTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ui/harness/UiTest.kt index 47c9f3c6ca..41ba6a14cd 100644 --- a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ui/harness/UiTest.kt +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/ui/harness/UiTest.kt @@ -34,25 +34,23 @@ abstract class UiTest { try { AppCrashes.install() AppCrashes.clear() - withKnownIssue(knownIssue.issues) { - try { - runInternalSkikoComposeUiTest(width = 1600, height = 900, coroutineDispatcher = StandardTestDispatcher()) { - mainClock.autoAdvance = false - UiScope(this, dir).apply { - body() - failOnPeerErrors(Peers.all()) - waitForAppWork() - failOnStuckWork(UiTestApp.drain()) - AppCrashes.failIfAny() - } + try { + runInternalSkikoComposeUiTest(width = 1600, height = 900, coroutineDispatcher = StandardTestDispatcher()) { + mainClock.autoAdvance = false + UiScope(this, dir).apply { + body() + failOnPeerErrors(Peers.all()) + waitForAppWork() + failOnStuckWork(UiTestApp.drain()) + AppCrashes.failIfAny() } - } catch (e: Throwable) { - throw if (e.suppressed.isEmpty()) e else AssertionError("${e.message}; suppressed:\n" + e.suppressed.joinToString("\n") { it.stackTraceToString().lines().take(25).joinToString("\n") }, e) } - repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} } - failOnStuckWork(UiTestApp.drain()) - AppCrashes.failIfAny() + } catch (e: Throwable) { + throw if (e.suppressed.isEmpty()) e else AssertionError("${e.message}; suppressed:\n" + e.suppressed.joinToString("\n") { it.stackTraceToString().lines().take(25).joinToString("\n") }, e) } + repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} } + failOnStuckWork(UiTestApp.drain()) + AppCrashes.failIfAny() passed = true } finally { repeat(2) { javax.swing.SwingUtilities.invokeAndWait {} } diff --git a/apps/multiplatform/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/AppCrashes.kt b/apps/multiplatform/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/AppCrashes.kt index 469b34cd33..fdfd553d3c 100644 --- a/apps/multiplatform/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/AppCrashes.kt +++ b/apps/multiplatform/common/src/uiTestShared/kotlin/chat/simplex/app/ui/harness/AppCrashes.kt @@ -1,8 +1,9 @@ package chat.simplex.app.ui.harness import org.junit.AssumptionViolatedException -import org.junit.rules.TestWatcher +import org.junit.rules.TestRule import org.junit.runner.Description +import org.junit.runners.model.Statement import java.util.concurrent.ConcurrentLinkedQueue @Target(AnnotationTarget.FUNCTION) @@ -10,17 +11,16 @@ import java.util.concurrent.ConcurrentLinkedQueue @Repeatable annotation class KnownAppIssue(val finding: String, val error: String, val intermittent: Boolean = false) -class KnownIssueRule : TestWatcher() { - var issues: List = emptyList() - private set +class KnownIssueRule : TestRule { + override fun apply(base: Statement, description: Description): Statement = object : Statement() { + override fun evaluate() = withKnownIssue(knownIssues(description)) { base.evaluate() } + } - override fun starting(description: Description) { - issues = description.annotations.flatMap { a -> - when { - a is KnownAppIssue -> listOf(a) - a.annotationClass.java.enclosingClass == KnownAppIssue::class.java -> (a.annotationClass.java.getMethod("value").invoke(a) as Array<*>).filterIsInstance() - else -> emptyList() - } + private fun knownIssues(description: Description): List = description.annotations.flatMap { a -> + when { + a is KnownAppIssue -> listOf(a) + a.annotationClass.java.enclosingClass == KnownAppIssue::class.java -> (a.annotationClass.java.getMethod("value").invoke(a) as Array<*>).filterIsInstance() + else -> emptyList() } } } diff --git a/plans/ui-tests/2026-10-10-desktop-ui-test-dsl-plan.md b/plans/ui-tests/2026-10-10-desktop-ui-test-dsl-plan.md index d83b764621..6230fd7a4e 100644 --- a/plans/ui-tests/2026-10-10-desktop-ui-test-dsl-plan.md +++ b/plans/ui-tests/2026-10-10-desktop-ui-test-dsl-plan.md @@ -1597,6 +1597,10 @@ Each entry gives the symptom, the cause (how it was confirmed), and the fix or m - Pausing the clock, as desktop does, breaks typing on Android instead (CI trial 6, 2026-10-23: 77 failures waiting for the message field to hold the typed text). The message field is an `AndroidView` `EditText`. Its update block reads `composeState`, so a text change re-runs it before the paused clock lets the field recompose. It then compares against `cs` captured at the last composition (`PlatformTextField.android.kt:69`, `:186-188`) and writes the old text back. - These three tests (`liveMessage_menuStart_alertConfirm_peerSeesStarted`, `liveMessage_boltButton_emptyStart`, `liveMessage_chatSwitch_sendsToOriginalChat`) type while the message is live, so they are skipped on Android with this reason (`assumeTrue`) and run on desktop. `liveMessage_received_showsLiveBadge` runs on both. +### 15.4.1e Open flake: 60 messages from a CLI peer stopped at 52 (Android CI, 1 of 2 runs) + +- `UnreadScrollTest.unread_openChat_scrollsToFirstUnread` (GitHub emulator, trial 7, 2026-10-23): bob's CLI sent `m1`..`m60` to alice; after 60 s alice's row showed `m52` with unread 52, and nothing more arrived. In trial 5 the same wait took 574 ms. Logcat has no error in that window, and the bridge log has no per-message lines. So it is not known whether bob's CLI did not send the last eight messages or alice did not receive them. Next step: log bob's sent-message count when the wait times out. + ### 15.4.2 Test strength limits (second review, 2026-10-14) Under the minimal-app-code rule (item 31) some states are asserted through the model, not the UI. These tests pass if the model is right even when the UI fails to render the state: