From f2d984244681440d0e0d7a15099316e2f308b3a8 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Thu, 15 Oct 2026 01:04:56 +0200 Subject: [PATCH 1/2] test(groupware): wait for contacts to render before deleting The contacts mutation scene tests acted after a fixed number of 16 ms frames while the synthetic store and transport answer each of six sequential requests after 20 ms. On a loaded machine the first click ran before the list (or the writable address book) had rendered, and the post-mutation assertions relied on forty fixed frames. Wait with the bounded settleUntil helper for the observable state each step needs: the contact row, an enabled Create contact control, and the reconciled terminal state after create and delete. Each wait is capped at 5 seconds and the existing assertions are unchanged. --- .../app/GroupwareContactsMutationSceneTest.kt | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt index 9227538ea..429b959e6 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/GroupwareContactsMutationSceneTest.kt @@ -3,6 +3,7 @@ package dev.obiente.nextcloudnative.app import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.semantics.getOrNull import androidx.compose.ui.text.AnnotatedString import java.lang.reflect.Proxy @@ -24,10 +25,15 @@ class GroupwareContactsMutationSceneTest { NativeGroupwareContactsScreen(fixture.services, fixture.session, "alice", {}, onMutationInProgressChanged = { blocked = it }) }) { + // The list appears only after the recovery read and the DAV discovery and listing requests. + settleUntil(SCENE_TIMEOUT_MILLIS) { has("Synthetic contact") } click("Synthetic contact") click("Delete") click("Delete") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.deletes > 0 && fixture.recovery == null && !blocked && !has("Synthetic contact") + } + settle() assertEquals(1, fixture.deletes) assertTrue(fixture.verifications >= 1, "Successful deletion must verify authoritative absence") assertNull(fixture.recovery) @@ -52,13 +58,19 @@ class GroupwareContactsMutationSceneTest { navigationEnabled = !blocked.value, content = workspace, ) }) { + // Creation is enabled only after the recovery read and the address book discovery complete. + settleUntil(SCENE_TIMEOUT_MILLIS) { enabled("Create contact") } + assertTrue(enabled("Create contact"), "Contacts did not finish loading a writable address book") click("Create contact") val name = generateSequence(requireNotNull(node("Name"))) { it.parent } .first { it.config.getOrNull(SemanticsActions.SetText)?.action != null } assertTrue(name.config[SemanticsActions.SetText].action!!.invoke(AnnotatedString("Synthetic contact"))) settle() click("Save") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.creates > 0 && fixture.recovery == null && !blocked.value && has("Synthetic contact") + } + settle() assertEquals(1, fixture.creates) assertNull(fixture.recovery) assertFalse(blocked.value) @@ -66,7 +78,10 @@ class GroupwareContactsMutationSceneTest { click("Synthetic contact") click("Delete") click("Delete") - repeat(4) { settle() } + settleUntil(SCENE_TIMEOUT_MILLIS) { + fixture.deletes > 0 && fixture.recovery == null && !blocked.value && !has("Synthetic contact") + } + settle() assertEquals(1, fixture.deletes) assertTrue(fixture.verifications >= 2) assertNull(fixture.recovery) @@ -76,6 +91,9 @@ class GroupwareContactsMutationSceneTest { } } + private fun NativeSceneTestDriver.enabled(label: String): Boolean = + node(label)?.let { SemanticsProperties.Disabled !in it.config } == true + private class Fixture { val session = NextcloudSession("https://contacts-scene.invalid", "alice", "synthetic") val home = "/remote.php/dav/addressbooks/users/alice/" @@ -130,4 +148,9 @@ class GroupwareContactsMutationSceneTest { private fun item(href: String, props: String) = "$href$propsHTTP/1.1 200 OK" private fun response(status: Int, body: String = "") = NextcloudApiResponse(status, body.encodeToByteArray(), "application/xml", "etag") } + + private companion object { + /** Bounds each wait for the delayed synthetic store and transport, which takes several frames. */ + const val SCENE_TIMEOUT_MILLIS = 5_000L + } } From 2c585f90cd2709627490e941fb4d1e3ce4a3a678 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Thu, 15 Oct 2026 02:35:28 +0200 Subject: [PATCH 2/2] docs(changes): record groupware contacts scene test wait fix --- changes/unreleased/groupware-contacts-scene-test-flake.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 changes/unreleased/groupware-contacts-scene-test-flake.md diff --git a/changes/unreleased/groupware-contacts-scene-test-flake.md b/changes/unreleased/groupware-contacts-scene-test-flake.md new file mode 100644 index 000000000..fcd09a676 --- /dev/null +++ b/changes/unreleased/groupware-contacts-scene-test-flake.md @@ -0,0 +1,7 @@ +category: internal +issue: none +pull: none +platforms: desktop +user-facing: no + +The desktop contacts mutation scene tests now wait for the rendered contact list and the reconciled end state instead of a fixed number of frames, so a slow machine no longer fails them.