- apps/multiplatform/scripts/waydroid: wd.sh (headless Waydroid for the Android UI tests and manual GUI testing), provision2.sh and watchdog2.sh for a second instance, weston.ini, fetch-images.sh, README; works on Qubes AppVMs and other Linux hosts (apt/dnf install, firewalld, own nft table, weston 10-14 flags); - uitest-android.sh uses it by default (WD_SH overrides) and takes adb from it, so both use one adb; - uitest.sh usage header; - plans/ui-tests: test drafts (including unmerged batch 3), the 2026-10-14 review reports, results after the rebase.
24 KiB
Shared UI test drafts: notes
14 tests in 11 classes. Nothing here has been compiled or run.
Paths:
C/meansapps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/H/meanstests/ChatTests/Smeansapps/multiplatform/common/src/commonMain/moko-resources/base/strings.xml- Line numbers are from
/home/user/wt-uitestateda94a505.
Files
| File | What it is |
|---|---|
main/ChatListToolbarSearchTest.kt |
searchFiltersByDisplayName |
main/ChatListMenusPendingTest.kt |
groupMenuMuteModesAllMentionsNone |
main/UserPickerTest.kt |
userPickerSwitchProfile |
main/ChatProfilesTest.kt |
profilesAddProfile |
main/ContactInfoTest.kt |
contactInfoVerifyCodeMatchesCli, contactInfoMarkVerified |
chat/EditMessageTest.kt |
edit_received_updatesBubble |
chat/DeleteMessageTest.kt |
delete_received_onlyForMe |
chat/ForwardTest.kt |
forward_contactToContact_peerSeesForwarded, forward_toNotes_showsSavedFrom |
chat/ChatSearchTest.kt |
search_direct_filtersResults |
chat/NotesTest.kt |
notes_sendText_saved |
groups/MemberInfoTest.kt |
memberInfo_changeRoleToAdmin, memberInfo_removeMember |
screens.kt |
A drop-in replacement for harness/Screens.kt. It keeps all of the current content unchanged and adds helpers (listed below). |
keys.diff |
The app-side keys. Paths are relative to /home/user/wt-uitest. git apply --check passes on eda94a505. |
Class names follow the catalogue class map in 00-legend.md, for example ChatListMenusPendingTest and DeleteMessageTest. The existing repo files use their own names instead: MessagingTest holds reply, edit, delete and reaction tests, and ChatListMenuTest and ContactsTest are used too. Some existing test names are not in the catalogue at all: contactMenuMarkReadAndUnread stands in for the catalogue's contactMenuMarkRead and contactMenuMarkUnread. If the traceability check is enforced, either the drafts get moved into the repo's classes or the repo's classes get renamed. That decision is left open.
Helpers added in screens.kt
ChatListScreenrowMenu(name, action, label)asserts the menu item's label before clicking it.search(text)shown(name)shareTo(name)
NewChatScreeninvite(vararg names)
UserPickerScreenprofiles()switchTo(name)
- New screens
ProfilesScreen:add(name),close()ContactInfoScreen:securityCode(),markVerified(),verifyRowText()GroupInfoScreen:member(name)MemberInfoScreen:changeRole(role),remove()
ChatScreencontactInfo(),groupInfo()item(id),itemTexts()forwardTo(text, chatName)search(text)
UiScope.ownGroup(app, group, vararg peers): ChatScreen- alice creates the group in the GUI and invites the peers from the invite sheet.
- Each peer runs
/j. - It returns the open group chat.
- It could move to
Helpers.kt, next tohostGroup.
Every helper uses only methods that both UiScope implementations have: click, rightClick, typeInto, escape, exists, waitFor, poll, textOf, textsOf, isEnabled, seesText, descriptionsOf, editableText and pressEnter. They also use CliPeer sends, eventually, eventuallyMessage, expects, expectsMessage and nextLine.
Keys added (keys.diff)
All keys are in commonMain. None of the touched composables has a desktopMain or androidMain actual, so both platforms get every key.
| Key | Tag | Attached at |
|---|---|---|
Ui.ChatList.Search |
chatList.search |
C/views/chatlist/ChatListView.kt:803-804, the SearchTextField modifier |
Ui.Chat.Bar.part("search") (no new constant) |
chat.bar.search |
C/views/helpers/DefaultTopAppBar.kt:87, the search field of any DefaultAppBar that has a uiKey (only ChatView passes one) |
Ui.Chat.BarMenu |
chat.bar.menu |
C/views/chat/ChatView.kt:1426, the toolbar ⋮ IconButton. Also adds import chat.simplex.common.ui.ui. |
Ui.Chat.BarMenuItem("search") |
chat.bar.menu.search |
ChatView.kt:1298 (direct) and :1328 (group) |
Ui.Chat.ItemMenu(id, "forward") |
chat.item.menu.forward[id] |
C/views/chat/item/ChatItemView.kt:478 |
Ui.Share.Row(chatId) |
share.row[chatId] |
C/views/chatlist/ShareListNavLinkView.kt. ShareListNavLinkLayout gets a uiKey parameter, and the Direct, Group and Local cases pass chat.id. |
Ui.Info.VerifyCode |
info.verifyCode |
C/views/chat/ChatInfoView.kt:1210-1218, inside VerifyCodeButton. The member-info screen shares this key; the two screens are never visible together. |
Ui.VerifyCode.Code |
verifyCode.code |
C/views/chat/VerifyCodeView.kt:80, the code Text |
Ui.VerifyCode.Mark |
verifyCode.mark |
VerifyCodeView.kt:117. SimpleButton (C/views/helpers/SimpleButton.kt:26) gets an optional uiKey parameter before click. I checked all 13 call sites and none of them is ambiguous. |
Ui.UserPicker.Profiles |
userPicker.profiles |
C/views/chatlist/UserPicker.kt:271-304, the "Your chat profiles" row |
Ui.Profiles.Add |
profiles.add |
C/views/usersettings/UserProfilesView.kt:182 |
Ui.NewProfile.Name |
newProfile.name |
C/views/WelcomeView.kt:139, CreateProfile |
Ui.NewProfile.Create |
newProfile.create |
C/views/WelcomeView.kt CreateProfile, the "Create profile" SettingsActionItem |
Ui.InviteMembers.Contact(contactId) |
inviteMembers.contact[id] |
C/views/chat/group/AddGroupMembersView.kt:342, ContactCheckRow. SectionItemViewWithoutMinPadding gets a uiKey parameter. |
Ui.InviteMembers.Invite |
inviteMembers.invite |
AddGroupMembersView.kt:243, inside InviteMembersButton |
Ui.GroupInfo.Member(groupMemberId) |
groupInfo.member[id] |
C/views/chat/group/GroupChatInfoView.kt:785, the member row. SectionItemViewLongClickable (C/views/helpers/Section.kt:240) gets a uiKey parameter. |
Ui.MemberInfo.Role (+ .option(GroupMemberRole)) |
memberInfo.role, memberInfo.role.option[Admin] |
C/views/chat/group/GroupMemberInfoView.kt:918, through the existing ExposedDropDownSettingRow uiKey |
Ui.MemberInfo.Remove |
memberInfo.remove |
GroupMemberInfoView.kt:843, RemoveMemberButton |
Ui.Alert.Option(Shared, "remove") |
alert.option.remove[Shared] |
GroupMemberInfoView.kt:314, the "Remove" button. It is keyed only in the group branch of removeMemberDialog; the relay and channel branches are not keyed. |
Kotlin naming: Ui.GroupInfo and Ui.Share are nested objects, so they never clash with the model class GroupInfo unless a file imports Ui.*. No file does that.
Per test
1. searchFiltersByDisplayName
Catalogue: 01:62. Needs P.
What the test asserts: with contact bob and the "Private notes" chat, typing "bo" into the chat-list search keeps bob's row and hides notes.
Why this is the expected behaviour:
filteredChats(C/views/chatlist/ChatListView.kt:1545-1573) keeps chats for whichanyNameContainsis true (C/model/ChatModel.kt:1420-1423,1921-1924).- "bo" has fewer than
MIN_NAME_LENGTH= 5 characters (ChatListView.kt:1145), sonameSearchCandidatereturns null. That means no name lookup and no "connect to name" row. - The notes chat's name is the string
note_folder_local_display_name= "Private notes" (S:90). It does not contain "bo". - Haskell:
testFilterSearch(H/ChatList.hs:82-95, search "bob" matches@bob).
Platform differences: none. The search bar is the same composable on both platforms; it sits at the bottom on Android when one-hand UI is on, but it is still composed.
Uncertainty:
- None in the behaviour.
- I used notes as the non-matching chat so that the row's Needs stays a single peer (P).
2. groupMenuMuteModesAllMentionsNone
Catalogue: 01:86. Needs P.
What the test asserts:
- The row menu item
ntfs.Mentionsis labelled "Mute", and choosing it setsenableNtfstoMentions. - Then
ntfs.Noneis labelled "Mute all" and setsNone. - Then
ntfs.Allis labelled "Unmute" and setsAll.
Why this is the expected behaviour:
GroupMenuItemselse-branch (C/views/chatlist/ChatListNavLinkView.kt:312-358) andToggleNotificationsChatAction(:435-446).MsgFilter.nextMode(true)andtext()(C/model/SimpleXAPI.kt:5456-5470).hasMentionsis true for every group (ChatModel.kt:1830).- Strings:
mute_chat,mute_all_chatandunmute_chat(S:855-857). - Setup: the same join flow as the existing
leaveGroup_fromChatListMenu. - The test then polls until membership is no longer
MemInvitedorMemAccepted, because only after that does the menu contain the ntfs item (ChatListNavLinkView.kt:320-335). - Haskell:
testMuteGroup(H/Direct.hs:1854). The CLI side prints nothing, so no CLI line is asserted.
Platform differences: none. Accepting the join alert does not open the chat (groupChatAction, :204-210), so the row stays visible on Android too.
Uncertainty: the catalogue also says "the row icon follows the mode". I did not assert the icon, for two reasons:
- The mentions icon and the muted icon have the same content description, "Notifications" (
C/views/chatlist/ChatPreviewView.kt:477-507), so the description cannot tell the modes apart. - If the joined group has unread items, the icon is replaced by the unread badge.
3. userPickerSwitchProfile
Catalogue: 01:119. Needs —.
What the test asserts:
- A group "team" exists for alice.
- alice creates the profile "work" in the GUI; "work" becomes the active profile and its chats load.
- alice opens the user picker and taps alice's avatar (
Ui.UserPicker.User(aliceId)). - alice is active again and the chat list shows "team".
Why this is the expected behaviour:
onUserClickedcallschangeActiveUser(C/views/chatlist/UserPicker.kt:160-176).changeActiveUser_reloads users and chats (SimpleXAPI.kt:748-778).- Avatars are keyed on desktop (
UserPicker.desktop.kt,UserPickerUsersSection) and on Android (UserPicker.android.kt:83). - Haskell:
testCreateSecondUser(H/Direct.hs:1968).
Platform differences:
- On desktop the inactive users appear in the picker's second section. On Android all users appear in the top section. The key is the same in both.
ProfilesScreen.close()usesescape(), which is the desktop backstack and the system back button on Android.
Uncertainty:
- "team" is created with
chatController.apiNewGroupbefore any UI step. This is deliberate: the group then reaches the model only through the reload that switching users triggers, so the finalrow("team")proves that the chat list was reloaded. - If the team prefers no direct API setup here, the alternative is
newChat().createGroup("team")followed by closing the invite sheet. That path needs a key or anescape()for the sheet.
4. profilesAddProfile
Catalogue: 01:122. Needs —.
What the test asserts:
- User picker → "Your chat profiles" → "Add profile" → name "work" → "Create profile".
- "work" becomes the current user, and the users list holds both "alice" and "work".
Why this is the expected behaviour:
addUser→CreateProfile(C/views/usersettings/UserProfilesView.kt:49-55,C/views/WelcomeView.kt:63-170).createProfileInProfilessetscurrentUserand refreshesusers(WelcomeView.kt:371-388).withAuthruns the block at once whenperformLAis off (C/views/usersettings/SettingsView.kt:475-478).- Strings:
your_chat_profiles(S:1011),users_add(S:2198),create_another_profile_button= "Create profile" (S:1256).
Platform differences: none in the test code. CreateProfile opens in ModalManager.center, which works on both platforms.
Uncertainty:
- "Create profile" is a
SettingsActionItem. When it is disabled it has no clickable modifier, so theDisabledsemantics never appear andisEnabledcannot be polled. The test relies ontypeIntohaving waited until the field holds the name. - The catalogue's "lists it" is checked through
chatModel.users, not the profile rows, because those rows have no key.
5. contactInfoVerifyCodeMatchesCli
Catalogue: 01:254. Needs P.
What the test asserts: the code shown on "Security code", with all whitespace removed, equals the line after alice security code: in bob's /code alice output, also with whitespace removed.
Why this is the expected behaviour:
VerifyCodeLayoutsplits the code into 24-character lines (C/views/chat/VerifyCodeView.kt:64-85).- The real CLI prints the label line and then the code (
src/Simplex/Chat/View.hs:2020-2023,viewSecurityCodewith the test view off). - Haskell:
testMarkContactVerified(H/Direct.hs:3116-3126,bCode=aCode).
Platform differences: none. Android also shows a "Scan code" button, which the test does not use.
Uncertainty:
- I used
eventually("alice security code:")rather thanexpects, becausesendsremoves only the echo and leaves any earlier lines in the queue (CliPeer.takeFirst). - The CLI's third line (the "pass this code…" hint) is not asserted.
6. contactInfoMarkVerified
Catalogue: 01:254. Needs P.
What the test asserts:
- "Mark verified" closes the screen.
- The model marks bob's contact as verified.
- The info row reads "View security code".
- Reopening the screen shows "bob is verified".
Why this is the expected behaviour:
VerifyCodeViewcloses on success (VerifyCodeView.kt:40-45).- The model is updated (
C/views/chat/ChatInfoView.kt:166-180), andContact.verifiedisactiveConn.connectionCode != null(ChatModel.kt:1919). - The row label is chosen by
VerifyCodeButton(ChatInfoView.kt:1210-1218). - Strings:
view_security_code(S:747),is_verified= "%s is verified" (S:1005).
Platform differences: none.
Uncertainty:
- The info screen must recompose from the model to show "View security code". The catalogue asserts this, but I did not trace how the
contactargument ofChatInfoLayoutis refreshed. - The final check finds "bob is verified" by text through
seesText; that line has no key.
7. edit_received_updatesBubble
Catalogue: 02:141. Needs P.
What the test asserts:
- bob sends "hello", then runs
! @alice hey, which edits his last message to alice. - The same item id in the GUI now has text "hey" and
meta.itemEditedset, and the bubble text contains "hey".
Why this is the expected behaviour:
EditMessageand its parser("! " <|> "!")(src/Simplex/Chat/Library/Commands.hs:2693,:6407).- Haskell:
testDirectMessageUpdate(H/Direct.hs:676-678,bob <# "alice> [edited] hey 👋"). AlsoH/Groups.hs:5926:##>shows that the CLI echoes!commands, sosendsfinds the echo. - The edited mark is an icon with no text or description (
C/views/chat/item/CIMetaView.kt:83-85), so it is checked through the model.
Platform differences: none.
Uncertainty: low. ASCII text avoids emoji input through the pty.
8. delete_received_onlyForMe
Catalogue: 02:151. Needs P.
What the test asserts:
- "Delete message?" offers
forMebut notforEveryone. - "Delete for me" removes the item from the model and from the screen.
- bob prints nothing.
Why this is the expected behaviour:
canDeleteForEveryone = meta.deletable && …(C/views/chat/item/ChatItemView.kt:1396-1427).deletable'is false for anything that is notCISndMsgContent(src/Simplex/Chat/Messages.hs:542-549).cidmInternalwith notoChatItem→removeChatItem(C/views/chat/ChatView.kt:548-581).- "bob prints nothing" is checked with a sentinel: alice then sends "ping", and bob's very next line must be
alice> ping.
Platform differences: rightClick is a long press on Android. The item's combinedClickable onLongClick opens the same menu (ChatItemView.kt:326-336).
Uncertainty: the sentinel assumes bob has no other queued lines after his own send. The existing reply_menu_sendQuoted_peerSeesQuote relies on the same assumption.
9. forward_contactToContact_peerSeesForwarded
Catalogue: 02:165. Needs P ×2.
What the test asserts:
- bob sends "hey"; alice forwards it to cath (menu "forward" → share row cath → Send).
- cath prints
alice> -> forwardedand thenhey.
Why this is the expected behaviour:
forwardItemsetschatId = nullandsharedContent = Forward(ChatView.kt:639-642).- The share list shows ready chats where sending is allowed (
C/views/chatlist/ShareListView.kt:196-202). - The compose area gets
ForwardingItems(C/views/chat/ComposeView.kt:1631-1634). Send is enabled while forwarding (ComposeView.kt:172-182), and the voice button is hidden (C/views/chat/SendMsgView.kt:73-74). - Sending forwards the item (
ComposeView.kt:838-850). - Haskell:
testForwardContactToContact(H/Forward.hs:167-177: bob's "hey" forwarded to cath printsalice> -> forwarded/hey).
Platform differences: none. On both platforms the share list replaces the chat list while sharedContent is set. Send is clicked rather than Enter, because the message field is empty.
Uncertainty: low.
10. forward_toNotes_showsSavedFrom
Catalogue: 02:167. Needs P.
What the test asserts:
- After forwarding bob's "hey" to "Private notes", the notes chat is open.
- It holds an item whose
itemForwardedisContact("bob"). - The bubble shows "saved from" and "bob".
Why this is the expected behaviour:
- The header has two rows when the chat type is Local and
chatTypeApiIdMsgIdis not null (C/views/chat/item/FramedItemView.kt:314-343). - Caption string
saved_from= "saved from" (S:73); the second row showsforwarded.chatName. - The core creates the note folder for every new user (
Commands.hs:471), so it exists for alice without a fixture. - Haskell:
testForwardContactToNotes(H/Forward.hs:229-247).
Platform differences: none.
Uncertainty: the header is matched as text inside Ui.Chat.Item(id). Both header rows are plain Text elements, so the match should hold, but it was not run.
11. search_direct_filtersResults
Catalogue: 02:230. Needs P.
What the test asserts:
- alice sends "hello there 🙂"; bob sends "hello there" and "how are you?".
- Searching "ello ther" leaves exactly {"hello there 🙂", "hello there"} in the model, and the "how are you?" item is no longer on screen.
Why this is the expected behaviour:
onSearchValueChanged→apiFindMessages, which clears the items and loads the search results (ChatView.kt:280-291,C/views/chatlist/ChatListNavLinkView.kt:254-260).- The ⋮ "Search" item is in
ChatView.kt:1296-1301. - Haskell:
testAddContact(H/Direct.hs:211-237,search=ello ther→[(1,"hello there 🙂"),(0,"hello there")]).
Platform differences: none. The ⋮ menu and the "Search" entry are common code for direct chats.
Uncertainty: the results are compared as a set, so their order is not asserted.
12. notes_sendText_saved
Catalogue: 02:276. Needs —.
What the test asserts: sending "keep in mind" in "Private notes" creates an item that exists on screen with direction LocalSnd.
Why this is the expected behaviour:
createNoteFolderContentItems(Commands.hs:5109).- Haskell:
testNotes(H/Local.hs:39-41).
Platform differences: none.
Uncertainty: none. The chat-list preview is not asserted, because on Android the list is not composed while a chat is open.
13. memberInfo_changeRoleToAdmin
Catalogue: 03:171. Needs P.
What the test asserts:
- alice creates #team and invites bob, who joins with
/j team. - alice opens Group info → bob → Change role → Admin → "Change".
- bob prints
#team: alice changed your role from member to admin (signed), and the model shows bob as Admin.
Why this is the expected behaviour:
onRoleSelected→updateMemberRoleDialog(confirmchange_verb) →updateMembersRole(C/views/chat/group/GroupMemberInfoView.kt:149-158,:968-991).- The role picker is shown for an owner (
canChangeRoleTo,ChatModel.kt:2944-2955). - The invite lines come from
fullAddMember(H/Utils.hs:917-928) andView.hs:1407(ttyFullGroup: no full name, because the GUI creates groups withfullName = "",C/views/newchat/AddGroupView.kt:159-161). - The join line comes from
createGroup2'(H/Utils.hs:822-825). - The role line comes from
testUpdateMemberRole(H/Groups.hs:1698-1701).
Platform differences: none. The group chat is still open after createGroup on both platforms, so ownGroup returns that chat screen instead of reopening it from the list.
Uncertainty:
- I assume the
ExposedDropdownMenuBoxpopup can be clicked on Android, as it already is inappearance_settingsPersistAcrossRestart. - I assume
option(GroupMemberRole.Admin)produces the tag[Admin]on both sides; enumtoStringreturns the name.
14. memberInfo_removeMember
Catalogue: 03:175. Needs P ×2.
What the test asserts:
- With bob and cath in alice's #team, Remove member → "Remove".
- bob prints
#team: alice removed you from the group (signed). - cath prints
#team: alice removed bob from the group (signed). - bob is no longer a current member in the model.
Why this is the expected behaviour:
removeMemberDialoggroup branch (GroupMemberInfoView.kt:304-333), thenremoveMember(:345-363).- String
remove_member_confirmation= "Remove" (S:2022). - Haskell:
testGroupRemoveAdd(H/Groups.hs:1148-1155).
Platform differences: none.
Uncertainty: cath's line depends on alice's broadcast reaching cath. Cath connects to alice when she joins, so the line does not depend on the bob–cath introduction finishing.
Rows considered and skipped
| Row | Reason |
|---|---|
delete_ownForMe_localOnly (02:150) |
The row's Needs is P, S (stable-compat class), which is outside the "—"/"P" scope. |
copy_text_toClipboardAndToast and all of 02 §N |
Every row needs CB and/or TOAST. |
| 02 §G receive-file rows | They are marked XFTP; the task's filter was strictly "—"/"P". |
contactInfoClearVerification (01:254) |
In scope, and the next one to add. It needs one more key, Ui.VerifyCode.Clear, on the "Clear verification" SimpleButton. |
forward_withComment_sendsCommentQuoted, forward_groupToContact, … (02:165-166) |
In scope and lower priority. They would reuse forwardTo plus typeInto(Ui.Chat.Message) before Send. |
search_group_filtersResults, search_close_restoresAll, search_notes_filtersResults (02:230-231) |
In scope. They reuse ChatScreen.search. The close test also needs a key on the search back button (NavigationButtonBack in ChatView.kt:1445). |
profilesActivateByTap, profilesSearchByName (01:122) |
In scope. They need a key on the UserProfilePickerItem rows (C/views/chatlist/UserPicker.kt:403). |
memberInfo_removeMemberWithMessages (03:175) |
Same setup as the remove test. It needs Ui.Alert.Option(Shared, "removeWithMessages") on the second button of removeMemberDialog. |
groupInfo_membersSectionCountAndSelfRow (03:68) |
The catalogue is wrong here. It says the self row reads «you: %1$s» "formatted with alice's name". The code formats it with member.memberStatus.shortText (C/views/chat/group/GroupChatInfoView.kt:1150-1151), so for the creator it reads "you: creator" (group_member_status_creator, S:1880). The catalogue row needs fixing before the test is written. |
toolbarMenu_muteUnmute_togglesNtfMode (02:28), groupInfo_muteCycleWithMentions (03:61) |
The mute modes are covered by test 2. These need keys on the toolbar ntfs item and on the group-info mute button. |
contactMenuMarkRead, contactMenuMarkUnread (01:84) |
Already covered in the repo by contactMenuMarkReadAndUnread, under a different name. |
searchFiltersByAlias, searchNoChatsFound, searchClearsWhenFilterChanges (01:62) |
In scope and cheap to add with ChatListScreen.search. searchNoChatsFound needs the "No filtered chats" text (S:504) or a key. |
userPickerUnreadBadgePerUser (01:120), profilesHide* (01:124) |
Lower value; the hide flows also need password-field keys. |
| Reports, moderation, support chats, channels, business (03 §12-14, §18-20) | They need ×2-×3 peers or RELAY; lower priority than the core flows. |