| name | ducko-ui |
| description | End-to-end UI testing for the Ducko macOS app using pre-built helper scripts. This skill should be used when the user asks to "test ducko", "test the app", "run E2E tests", "test the app end-to-end", "test the UI", "send a test message", or wants to verify DuckoApp works from login through messaging. |
Ducko UI Test
Run the /macos-ui-testing skill first to load generic macOS UI automation patterns. This skill provides Ducko-specific helper scripts on top.
Each script wraps a single osascript flow so it can be allowlisted in settings.local.json (see Permission Allowlisting at the end).
Window Architecture
DuckoApp ships separate windows:
- Contact List (
id: "contacts") — singleton, main window after login (roster, status picker, search).
- Chat (
id: "chat") — singleton tabbed window holding every open conversation as a bottom tab (chat-tab-bar); opened/raised by double-click or New Chat. Switching tabs preserves each conversation's draft, search, and sidebar state.
- Contact Info (
id: "contact-info", keyed by ContactInfoRef) — Get Info window: identity, roster/subscription state, vCard, and Block/Remove. Opened from the chat header (i) button or a contact's "Get Info" context item.
- Chat Transcripts (
id: "transcripts") — singleton history window; the header clock and a contact's "History" context item retarget it to that conversation.
- MenuBarExtra — quick status, Show Contact List, Quit.
The Contacts window has no toolbar. Its actions live in the app menu bar: New Chat (⌘N), Join Room (⌘⇧N), Bookmarks (⌘⇧B) under File; Add Contact (⌘D), My Profile under the Contact menu; sort order + Hide Offline under View. ⌘F reveals the roster search field (contact-search-field). Scripts drive these via keyboard shortcuts or menu-bar clicks (menu bar item ... of menu bar 1), not window buttons.
Prerequisites
- Peekaboo CLI installed (
/opt/homebrew/bin/peekaboo)
- Accessibility permissions granted for Terminal/Claude
Scripts
All scripts are in scripts/ relative to this skill. Run from the repo root or use absolute paths.
Scripts target SwiftUI accessibility identifiers, not positional selectors.
Accessibility Identifiers
| Identifier | Element | Window |
|---|
contact-list | Contact list view | Contacts |
contact-row-{jid} | Individual contact row (account-qualified as contact-row-{jid}|{account-jid} when the JID is on more than one account) | Contacts |
status-picker | Presence status pull-down (status rows with colored dots, saved statuses, per-account override submenus, Custom…) in the "me" header | Contacts |
custom-status-message-field | Message field in the status pull-down's Custom… sheet | Contacts |
identity-switcher | Identity (account) switcher menu on the "me" header name, shown with 2+ enabled accounts | Contacts |
my-avatar | Self avatar in the "me" header | Contacts |
status-preferences | Saved-status management list in Preferences ▸ Status | Preferences |
saved-status-message-field | Message field in the Preferences ▸ Status add sheet | Preferences |
contact-search-field | Roster search field, revealed by ⌘F | Contacts |
message-field | Message input text field | Chat |
send-button | Send message button | Chat |
new-chat-jid-field | JID field in New Chat sheet | Contacts |
new-chat-account-picker | Account picker in New Chat sheet (shown only when more than one account is enabled) | Contacts |
start-chat-button | Start Chat button in sheet | Contacts |
add-contact-jid-field | JID field in Add Contact sheet | Contacts |
add-contact-button | Add Contact button in sheet | Contacts |
rename-contact-field |
Context Menu Features
Contact Row
Right-click a contact row in the contact list:
- Start Chat — open a chat tab with the contact
- Get Info — open the Contact Info window (identity, subscription, vCard, Block/Remove)
- History — open the transcript window scoped to this contact
- Pin / Unpin — pin or unpin the contact to the top of the list
- Mute / Unmute — mute or unmute notifications
- Rename — set a local alias for the contact
- Block / Unblock — block or unblock the contact
- Remove Contact — remove from roster
Room Row
Right-click a room row in the Rooms section:
- Open Chat — open the room chat window
- Pin / Unpin — pin or unpin the room to the top of the list
- Mute / Unmute — mute or unmute notifications
- Invite User — invite a JID to the room
- Room Settings — open room config sheet (owner only)
- Leave Room — leave the room
Participant Sidebar
Right-click a participant in the chat window sidebar:
- Kick — kick participant (moderator required)
- Ban — ban participant (admin/owner required)
- Grant Voice — grant voice to visitor (moderator required)
- Revoke Voice — revoke voice from participant (moderator required)
- Change Nickname — change own nickname (self only)
Script Reference
| Script | Purpose | Arguments |
|---|
ducko-launch.sh | Build and launch DuckoApp, output window ID | none |
ducko-import.sh | Switch to Import mode and click Import button | none |
ducko-select-mode.sh | Select a setup mode segment on the Welcome screen | <Import|Login|Register> |
ducko-login.sh | Fill JID + password, click Connect | JID PASSWORD |
ducko-new-chat.sh | Open New Chat sheet from contact list, fill JID, optionally pick the sending account, start chat | JID [ACCOUNT] |
ducko-add-contact.sh | Open Add Contact sheet from contact list, fill JID, submit | JID |
ducko-send.sh | Type a message and send it in the active chat window | MESSAGE |
ducko-screenshot.sh | Capture window screenshot | [FILENAME] (optional, absolute path or relative to /private/tmp/claude/) |
ducko-search.sh | Toggle Cmd+F search bar in chat, optionally search | [QUERY] (optional) |
ducko-contact-search.sh | Reveal the contact-list search (⌘F), optionally filter the roster | [QUERY] (optional) |
ducko-reply.sh | Right-click a message and select Reply | [TEXT] (optional, matches message containing TEXT; default: last message) |
ducko-sort.sh | Open the View menu (Sort Contacts / Hide Offline), optionally select sort/filter | [alphabetical|byStatus|recentConversation|hideOffline] (optional) |
ducko-join-room.sh | Open Join Room sheet, fill room JID + nickname, join | ROOM_JID [NICKNAME] |
|
Workflow
Fresh install E2E test
For first-time setup when no account exists:
Complete the Welcome screen first. On a fresh profile the first-run Welcome onboarding window holds key/first-responder status. Contact-list actions like Add Contact are gated behind a focus check (Contacts must be the key window), and synthetic automation can't force that focus while Welcome is up — so contact/chat steps silently fail until Welcome is gone. Welcome can't simply be dismissed; you must complete it by logging in through it. The Welcome screen defaults to Import mode, and ducko-login.sh assumes Login mode is already selected, so select the Login segment of the setup-mode-picker segmented control before running it.
The Welcome scripts (ducko-login.sh, ducko-register.sh, ducko-import.sh) walk the Welcome window via the recursive findByAttr handler (like ducko-select-mode.sh) and gate their success message on the osascript result, so a missing field reports an error instead of false success. The login screen's identifiers are covered by UILoginTests (which drives them through the test harness's Swift AX helpers, not this script); the register/import segments have no integration test, so for all three the script's traversal and success-gating are verified by construction.
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-select-mode.sh Login
$SCRIPTS/ducko-login.sh "USER_JID" "PASSWORD_HERE"
$SCRIPTS/ducko-screenshot.sh "after-login.png"
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-send.sh "Hello from Ducko!"
$SCRIPTS/ducko-send.sh "Testing 1-2-3"
$SCRIPTS/ducko-screenshot.sh "after-messages.png"
$SCRIPTS/ducko-stop.sh
Relaunch E2E test (existing account)
When an account already exists, the app auto-connects on launch using Keychain credentials. No login step needed:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-screenshot.sh "after-relaunch.png"
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-send.sh "Relaunch test message"
$SCRIPTS/ducko-stop.sh
Quick message test (chat window already open)
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-send.sh "Quick test message"
$SCRIPTS/ducko-screenshot.sh
Chat UI polish test (message grouping, search, reply)
Tests message grouping, search bar, reply compose bar, and context menu:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-send.sh "Message one"
$SCRIPTS/ducko-send.sh "Message two"
$SCRIPTS/ducko-send.sh "Message three"
$SCRIPTS/ducko-screenshot.sh "grouped-messages.png"
$SCRIPTS/ducko-search.sh "Message two"
$SCRIPTS/ducko-screenshot.sh "search-results.png"
$SCRIPTS/ducko-search.sh
$SCRIPTS/ducko-reply.sh "Message one"
$SCRIPTS/ducko-screenshot.sh "reply-bar.png"
$SCRIPTS/ducko-stop.sh
Contact list sort/filter test
Tests sort modes and hide-offline toggle:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-sort.sh
$SCRIPTS/ducko-screenshot.sh "sort-menu.png"
$SCRIPTS/ducko-sort.sh byStatus
$SCRIPTS/ducko-screenshot.sh "sorted-by-status.png"
$SCRIPTS/ducko-sort.sh hideOffline
$SCRIPTS/ducko-screenshot.sh "hide-offline.png"
$SCRIPTS/ducko-stop.sh
MUC (Group Chat) test
Tests MUC features — join room, send group message, toggle participant sidebar:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-join-room.sh "room@conference.example.com" "mynick"
$SCRIPTS/ducko-screenshot.sh "muc-joined.png"
$SCRIPTS/ducko-send.sh "Hello room!"
$SCRIPTS/ducko-screenshot.sh "muc-chat.png"
$SCRIPTS/ducko-toggle-sidebar.sh
$SCRIPTS/ducko-screenshot.sh "muc-sidebar.png"
$SCRIPTS/ducko-focus-contacts.sh
$SCRIPTS/ducko-screenshot.sh "muc-rooms-section.png"
$SCRIPTS/ducko-stop.sh
Room settings test
Tests room settings — open settings sheet from context menu, view config and affiliations:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-join-room.sh "room@conference.example.com" "mynick"
$SCRIPTS/ducko-focus-contacts.sh
$SCRIPTS/ducko-room-settings.sh "room@conference.example.com"
$SCRIPTS/ducko-screenshot.sh "room-settings-general.png"
$SCRIPTS/ducko-room-settings-tab.sh Members
$SCRIPTS/ducko-screenshot.sh "room-settings-members.png"
$SCRIPTS/ducko-stop.sh
Preferences window test
Tests the Preferences window — open via Cmd+,, navigate tabs, verify account management:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-preferences.sh
$SCRIPTS/ducko-screenshot.sh "preferences-general.png"
$SCRIPTS/ducko-preferences-tab.sh Accounts
$SCRIPTS/ducko-screenshot.sh "preferences-accounts.png"
$SCRIPTS/ducko-preferences-tab.sh Chat
$SCRIPTS/ducko-screenshot.sh "preferences-chat.png"
$SCRIPTS/ducko-preferences-tab.sh Appearance
$SCRIPTS/ducko-screenshot.sh "preferences-appearance.png"
$SCRIPTS/ducko-preferences-tab.sh Notifications
$SCRIPTS/ducko-screenshot.sh "preferences-notifications.png"
$SCRIPTS/ducko-preferences-tab.sh Advanced
$SCRIPTS/ducko-screenshot.sh "preferences-advanced.png"
$SCRIPTS/ducko-stop.sh
Profile editing test
Tests the vCard profile editing flow — open profile sheet, verify fields:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-profile.sh
$SCRIPTS/ducko-screenshot.sh "profile-edit.png"
$SCRIPTS/ducko-stop.sh
Avatar upload/remove test
Tests avatar upload and removal via the profile sheet:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-profile.sh
$SCRIPTS/ducko-avatar.sh "/path/to/test-image.png"
$SCRIPTS/ducko-screenshot.sh "avatar-uploaded.png"
$SCRIPTS/ducko-avatar-remove.sh
$SCRIPTS/ducko-screenshot.sh "avatar-removed.png"
$SCRIPTS/ducko-stop.sh
Device trust management test
Tests OMEMO encryption UI — encryption menu, device fingerprints sheet, and trust actions:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-screenshot.sh "encryption-menu.png"
$SCRIPTS/ducko-encrypt.sh fingerprints
$SCRIPTS/ducko-screenshot.sh "device-fingerprints.png"
$SCRIPTS/ducko-screenshot.sh "device-trust-status.png"
$SCRIPTS/ducko-preferences.sh
$SCRIPTS/ducko-preferences-tab.sh Chat
$SCRIPTS/ducko-screenshot.sh "encryption-preferences.png"
$SCRIPTS/ducko-stop.sh
Presence/status test
Tests setting presence status and status message:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-status.sh away
$SCRIPTS/ducko-screenshot.sh "status-away.png"
$SCRIPTS/ducko-status.sh dnd "In a meeting"
$SCRIPTS/ducko-screenshot.sh "status-dnd-message.png"
$SCRIPTS/ducko-status.sh available
$SCRIPTS/ducko-screenshot.sh "status-available.png"
$SCRIPTS/ducko-stop.sh
Bookmarks management test
Tests bookmark lifecycle — open bookmarks, add, verify, remove:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-bookmarks.sh
$SCRIPTS/ducko-screenshot.sh "bookmarks-list.png"
$SCRIPTS/ducko-add-bookmark.sh "room@conference.example.com" "mynick"
$SCRIPTS/ducko-screenshot.sh "bookmark-added.png"
$SCRIPTS/ducko-remove-bookmark.sh "room@conference.example.com"
$SCRIPTS/ducko-screenshot.sh "bookmark-removed.png"
$SCRIPTS/ducko-stop.sh
Message retraction test
Tests sending and retracting a message:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-send.sh "This message will be retracted"
$SCRIPTS/ducko-screenshot.sh "before-retract.png"
$SCRIPTS/ducko-retract.sh "This message will be retracted"
$SCRIPTS/ducko-screenshot.sh "after-retract.png"
$SCRIPTS/ducko-stop.sh
OMEMO encryption test
Tests encryption toggle, device fingerprints sheet, and device trust:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-encrypt.sh on
$SCRIPTS/ducko-screenshot.sh "encryption-enabled.png"
$SCRIPTS/ducko-encrypt.sh fingerprints
$SCRIPTS/ducko-screenshot.sh "device-fingerprints.png"
$SCRIPTS/ducko-stop.sh
Room management test
Tests full room lifecycle — join, topic, nickname, file attachment, leave:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-join-room.sh "room@conference.example.com" "mynick"
$SCRIPTS/ducko-room-topic.sh
$SCRIPTS/ducko-screenshot.sh "room-topic-view.png"
$SCRIPTS/ducko-room-topic.sh "Welcome to the test room!"
$SCRIPTS/ducko-screenshot.sh "room-topic-set.png"
$SCRIPTS/ducko-toggle-sidebar.sh
$SCRIPTS/ducko-change-nickname.sh "newnick"
$SCRIPTS/ducko-screenshot.sh "nickname-changed.png"
$SCRIPTS/ducko-focus-contacts.sh
$SCRIPTS/ducko-leave-room.sh "room@conference.example.com"
$SCRIPTS/ducko-screenshot.sh "room-left.png"
$SCRIPTS/ducko-stop.sh
Live-update (cross-driver) test
Verifies that an open chat/room window reflects server-pushed changes (subject, participant list, presence) live, without reopening the tab — by driving a second occupant from DuckoCLI while the GUI stays joined. This exercises the live-read path that backs the chat header, window title, and participant sidebar.
SCRIPTS="Skills/ducko-ui/scripts"
DUCKO_PROFILE=clibot .build/debug/DuckoCLI account add ...
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-join-room.sh "room@conference.example.com" "observer"
$SCRIPTS/ducko-screenshot.sh "live-before.png"
printf '/join room@conference.example.com clibot\n' \
| DUCKO_PROFILE=clibot .build/debug/DuckoCLI interactive
osascript -e 'tell application "System Events" to set frontmost of process "DuckoApp" to true'
$SCRIPTS/ducko-screenshot.sh "live-after.png"
$SCRIPTS/ducko-stop.sh
- The GUI must be frontmost while observing. DuckoApp sends XEP-0352
CSI inactive whenever it is not the frontmost app, and the server then queues MUC presence/subject pushes, flushing them only on CSI active. A background-driven observation sees a stale header/subject and wrongly concludes the live-read is broken — activate the app (set frontmost) before screenshotting; do not relaunch.
- Subject-change variant. Ad-hoc MUC rooms restrict
/topic to moderators, so the first occupant to join a fresh room owns it. To test a live subject update, have account B join the empty room first (becoming owner) and keep its REPL session open while the GUI joins as a guest, then issue /topic <subject> and assert room-subject-view updates live. Participant-count and presence updates (above) need no ownership and work from any second occupant.
- Use a separate
DUCKO_PROFILE for the CLI account so its credentials and SwiftData don't collide with the GUI profile.
File attachment test (full)
Tests attaching and sending a file:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-new-chat.sh "CHAT_PARTNER_JID"
$SCRIPTS/ducko-attach.sh "/path/to/test-file.png"
$SCRIPTS/ducko-screenshot.sh "file-attached.png"
$SCRIPTS/ducko-send.sh "Here's a file"
$SCRIPTS/ducko-screenshot.sh "file-sent.png"
$SCRIPTS/ducko-stop.sh
Connection info test
Tests viewing TLS connection details:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-launch.sh
$SCRIPTS/ducko-connection-info.sh
$SCRIPTS/ducko-screenshot.sh "connection-info.png"
$SCRIPTS/ducko-stop.sh
Reconnect (app disconnected)
If the app is running but disconnected (e.g. network drop), restart it to trigger auto-connect:
SCRIPTS="Skills/ducko-ui/scripts"
$SCRIPTS/ducko-connect.sh
Permission Allowlisting
To allow these scripts in settings.local.json without prompts:
{
"permissions": {
"allow": [
"Bash(Skills/ducko-ui/scripts/*)"
]
}
}
Notes
- Scripts use
keystroke (not set value) to trigger SwiftUI bindings.
- App activation uses
set frontmost of process (works with SwiftPM builds).
- When the app is not frontmost it sends XEP-0352
CSI inactive, and the server queues MUC presence/subject pushes until CSI active — so verifying any live update (subject, participant count, presence) requires the app frontmost first, or the observation reads stale.
- Multi-step interactions are bundled in single osascript blocks to avoid focus loss.
- Element targeting uses recursive UI-element-tree walks (
findByAttr and siblings) rather than entire contents, which silently collapses on the deeply-nested SwiftUI / NSTableView accessibility trees on macOS 26 (contact/room rows, the Welcome window, chat transcript, profile/room-settings sheets). Helper-consumer scripts share these handlers via ducko-helpers.sh's ducko_as_handlers; quoted-heredoc scripts (the Welcome family, profile/room-settings, etc.) inline the same handler — mirroring ducko-select-mode.sh — because a quoted heredoc can't expand $(...). Scripts whose targets are shallow (small sheets like New Chat, Add Contact, Join Room) still use entire contents where it reliably reaches them. SwiftUI segmented Picker segments and Room Settings tabs expose their label via AXDescription (not name/AXTitle), so they are matched on that.
- The scripts marked best-effort in the Script Reference table (
ducko-status.sh, ducko-chat-tabs.sh, ducko-connection-info.sh, ducko-change-password.sh) hit osascript limitations covered authoritatively by the integration suite instead; each table row names the specific control and test.
- The contact list and chat windows are both singletons (
Window). The chat window holds all open conversations as bottom tabs (chat-tab-bar); ducko-send.sh targets the active tab in the frontmost chat window. Contact Info is a WindowGroup keyed by ContactInfoRef.
- Arguments passed via
osascript - "$ARG" << 'APPLESCRIPT' + on run argv (no shell injection).
- Credentials are arguments, never hardcoded.