Automatyzacja interfejsu użytkownika

Sprawdzanie i interakcja z uruchamianiem aplikacji Windows z poziomu wiersza polecenia. Używane przez agentów sztucznej inteligencji i deweloperów do testowania, debugowania i automatyzacji interfejsu użytkownika.

Przegląd

winapp ui udostępnia polecenia do inspekcji i interakcji z interfejsami użytkownika aplikacji Windows. Używa Windows automatyzacja interfejsu użytkownika (UIA). Współpracuje z dowolną aplikacją Windows — WPF, WinForms, Win32, Electron i WinUI 3. Większość poleceń napędza aplikację za pomocą wzorców interfejsu użytkownika (bez iniekcji danych wejściowych); ui click jest wyjątkiem i używa rzeczywistej symulacji myszy dla kontrolek, które nie obsługują InvokePattern.

Szybki start

# Connect to any app and see its UI tree
winapp ui inspect -a notepad

# Find specific elements
winapp ui search Button -a notepad

# Activate an element
winapp ui invoke Close -a notepad

# Take a screenshot
winapp ui screenshot -a notepad

Określanie wartości docelowej aplikacji

Według nazwy procesu

winapp ui inspect -a notepad
winapp ui inspect -a slack            # auto-picks visible window for multi-process apps
winapp ui inspect -a imageresizer     # partial match: finds PowerToys.ImageResizer

Według tytułu okna

winapp ui inspect -a "LICENSE - Notepad"
winapp ui inspect -a "Fix WinApp"     # partial title match

Według identyfikatora PID

winapp ui inspect -a 12345

Przez HWND (stabilny — przetrwa zmiany tabulatora/tytułu)

# Discover HWNDs
winapp ui list-windows -a Terminal
  → HWND 985238: "🤖 Testing" (WindowsTerminal, PID 21228)
  → HWND 131906: "Fix WinApp" (WindowsTerminal, PID 21228)

# Target specific window
winapp ui inspect -w 131906
winapp ui screenshot -w 131906

Służy -a do odnajdywania w -w celu uzyskania stabilnego określania wartości docelowej. Gdy -a pasuje do wielu okien, polecenie wyświetla je z HWNDs, które chcesz wybrać.

Selektory

Elementy docelowe przy użyciu selektora wyświetlanego w [brackets] danych wyjściowych inspekcji/wyszukiwania. Istnieją trzy typy selektorów:

Selector Znaczenie Example
MinimizeButton AutomationId (wyświetlany, gdy unikatowy — stabilny, preferowany) winapp ui invoke MinimizeButton -a myapp
btn-close-d1a0 Slug semantyczny (pokazany, gdy nie ma unikatowego identyfikatora AutomationId) winapp ui invoke btn-close-d1a0 -a myapp
Submit Wyszukiwanie w postaci zwykłego tekstu względem parametru Name/AutomationId (podciąg bez uwzględniania wielkości liter) winapp ui invoke Submit -a myapp

Selektory AutomationId to identyfikatory zestawu deweloperów (AutomationProperties.AutomationId w języku XAML). Gdy identyfikator AutomationId jest unikatowy w całym drzewie interfejsu inspect użytkownika i search wyświetla go bezpośrednio jako selektor — te zmiany układu przetrwania, lokalizacja i restrukturyzacja drzewa.

Selektory Slug (np. ) są generowane, btn-close-d1a0gdy nie istnieje unikatowy identyfikator AutomationId. Format: prefix-name-hash. Skrót weryfikuje tożsamość elementu, ale może przestarzał po zmianie interfejsu użytkownika.

Sprawdzanie formatu danych wyjściowych

Polecenie inspect wyświetla drzewo elementów z kolorowymi danymi wyjściowymi (selektor w cyjanku, nazwa na zielono, metadane w kolorze szarym):

TabView Tab (0,-1 1200x48)
  TabListView List (4,-1 1100x48)
    tab-newtab-5f5b TabItem "New Tab" (14,-1 200x48)
  NewTabButton SplitButton "New Tab" [collapsed] (1104,5 96x36)
Found 10 elements (--depth 3). Use the first token as selector, e.g.: winapp ui invoke TabView -a terminal

Pierwszym wyrazem w każdym wierszu jest selektor — użyj go z innymi ui poleceniami. Gdy element ma unikatowy identyfikator AutomationId, jest używany bezpośrednio (np. TabView, NewTabButton). Jeśli nie istnieje unikatowy identyfikator AutomationId, jest używany wygenerowany ślimak (np. tab-newtab-5f5b).

Slugi semantyczne

Slugi używają formatu: prefix-normalizedname-hash gdzie:

  • prefiks — 3-literowy skrót typu (btn, txt, chk, cmb, itm, tab, img, lbl, pn, win, grp, lnk, mnu itp.)
  • normalizedname — małe litery alfanumeryczne z identyfikatora AutomationId (preferowane) lub nazwa, maksymalnie 15 znaków
  • hash — skrót 4-znakowy skrót szesnastkowy identyfikatora RuntimeId elementu (weryfikuje tożsamość elementu)

Slugi są bezpieczne za pomocą powłoki (bez znaków specjalnych), unikatowe i mogą być używane bezpośrednio jako argumenty. Skrót zapewnia wykrywanie nieaktualności — jeśli element został zastąpiony, otrzymasz: "Element mógł ulec zmianie. Uruchom ponownie inspekcję".

Elementy bez nazwy lub AutomationId pokazują tylko prefiks + skrót (np. pn-c8a3).

Uściślanie wielu dopasowań

Slugi z inspect/search danych wyjściowych są unikatowe, ale mogą zmieniać się między zmianami układu — używają ich w zwykłych nazwach typów lub tekście, gdy wiele dopasowań. Gdy selektor jest niejednoznaczny, interfejs wiersza polecenia drukuje wszystkie dopasowania ze swoimi ślimakami, aby można było wybrać właściwy i ponownie uruchomić z tym ślimakiem.

winapp ui search Button -a myapp            # shows: btn-ok-a1b2 "OK", btn-cancel-c3d4 "Cancel"
winapp ui invoke btn-ok-a1b2 -a myapp       # invoke using slug (preferred)
winapp ui invoke btn-cancel-c3d4 -a myapp   # invoke the other Button by its slug

Użyj zwykłego tekstu, aby wyszukać elementy — nie jest wymagana żadna specjalna składnia:

winapp ui search Minimize -a notepad        # finds elements with "Minimize" in Name or AutomationId
winapp ui search Close -a notepad           # case-insensitive substring match
winapp ui invoke Minimize -a notepad        # search + invoke in one step (disambiguates if needed)
winapp ui search "Save" -a notepad          # find elements containing "Save"
winapp ui search "error" -a myapp           # case-insensitive match

Gdy wyszukiwanie tekstu pasuje do wielu elementów (np. SettingsExpander, gdzie grupa, przycisk i tekst mają taką samą nazwę), interfejs wiersza polecenia automatycznie wybiera jedyny element wywołujący. Jeśli wiele jest wywoływanych, wyświetla listę wszystkich dopasowań z ślimakami.

W przypadku wyników wyszukiwania niepodwoływalnych (np. textblock wewnątrz przycisku) wyszukiwanie automatycznie wyświetla najbliższy nadrzędny element — element nadrzędny, którego można użyć z elementem invoke. Działa to dla wszystkich selektorów wyszukiwania:

  lbl-savechanges-a1b2 "Save changes" (120,40 80x20)
        ^ invoke via: btn-save-c3d4 "Save"

Selektor powierzchniowy może być używany bezpośrednio:

winapp ui invoke btn-save-c3d4 -a myapp    # invoke the parent Button

Komendy

stan

Połącz się z aplikacją i pokaż informacje o połączeniu.

winapp ui status -a notepad
winapp ui status -a notepad --json

Sprawdzić

Wyświetl drzewo elementów interfejsu użytkownika. Dane wyjściowe pokazują semantycznie slugi z wcięciem 2-spacji dla hierarchii:

winapp ui inspect -a notepad                    # full window tree, depth 3
winapp ui inspect -a notepad --depth 5          # deeper tree
winapp ui inspect txt-searchbox-e5f6 -a notepad # subtree rooted at element
winapp ui inspect --ancestors btn-close-d1a2 -a notepad  # walk up from element to root
winapp ui inspect -a myapp --interactive        # invokable elements only, auto-depth 8
winapp ui inspect -a myapp --hide-disabled      # hide disabled elements
winapp ui inspect -a myapp --hide-offscreen     # hide offscreen elements

Przykładowe dane wyjściowe (wartość domyślna):

win-aidevgalleryp-f1a3 "AI Dev Gallery Preview" (94,206 1280x1023)
  pn-c8a3 (102,207 1264x1014)
    btn-minimize-d1a0 "Minimize" (1222,206 48x48)
    btn-maximize-e2b1 "Maximize" (1270,206 48x48)
    itm-samples-3f2c "Samples" (102,330 72x62)

Przykładowe dane wyjściowe (--interactive — tylko elementy, lista płaska):

btn-minimize-d1a0 "Minimize" (1222,206 48x48)
btn-maximize-e2b1 "Maximize" (1270,206 48x48)
btn-close-d1a2 "Close" (1318,206 48x48)
itm-home-7b3e "Home" (102,268 72x62)
itm-samples-3f2c "Samples" (102,330 72x62)
itm-models-9a4f "Models" (102,392 72x62)

Elementy mogą pokazywać następujące znaczniki stanu:

  • [on] / [off] / [indeterminate] — stan przełącznika/pola wyboru
  • [collapsed] / [expanded] — stan rozwijania/zwijania drzew, pól kombi, elementów menu
  • [scroll:v] / [scroll:h] / [scroll:vh] — kontener przewijany (pionowy, poziomy lub oba)
  • [offscreen] — element nie jest widoczny na ekranie
  • [disabled] — element nie jest włączony
  • value="..." — bieżąca zawartość tekstowa elementów edytowalnych (gdy różni się od nazwy)

Znajdowanie elementów pasujących do selektora. Dane wyjściowe pokazują semantycznie slugi:

winapp ui search Button -a notepad              # all buttons
winapp ui search Close -a notepad               # finds elements with "Close" in name
winapp ui search SearchBox -a notepad           # finds elements with "SearchBox" in name or AutomationId
winapp ui search Button --max 10 -a notepad     # limit results

Przykładowy wynik:

  btn-minimize-d1a0 "Minimize" (1222,206 48x48)
  btn-maximize-e2b1 "Maximize" (1270,206 48x48)
  btn-close-d1a2 "Close" (1318,206 48x48)

Slugi wyświetlane w danych wyjściowych (np. btn-minimize-d1a0) mogą być używane bezpośrednio z innymi poleceniami:

winapp ui invoke btn-minimize-d1a0 -a notepad

get-property

Odczytywanie wartości właściwości z elementu. Obejmuje stan specyficzny dla wzorca (ToggleState, Value, IsSelected itp.).

winapp ui get-property btn-submit-7a90 -a myapp              # all properties
winapp ui get-property chk-checkbox-b2c3 -p ToggleState -a myapp   # checkbox state
winapp ui get-property txt-textbox-a4b1 -p Value -a myapp          # current text value
winapp ui get-property cmb-combobox-d5e6 -p ExpandCollapseState -a myapp  # expanded or collapsed

zrzut ekranu

Przechwyć okno lub element jako PNG. Gdy istnieje wiele okien (np. aplikacja + otwarte okno dialogowe), są one złożone w jeden plik PNG z każdym oknem zeszytymi.

winapp ui screenshot -a notepad                     # saves screenshot.png in cwd
winapp ui screenshot -a notepad --output my.png     # custom filename
winapp ui screenshot -a notepad --json              # returns file path as JSON
winapp ui screenshot -w 131906                      # target specific HWND (+ its dialogs)
winapp ui screenshot txt-searchbox-e5f6 -a myapp          # crop to element bounds
winapp ui screenshot -a myapp --capture-screen      # capture from screen (includes popups/overlays)

Gdy okna dialogowe lub wyskakujące okienka są otwarte, wszystkie okna są złożone w jeden plik PNG, dzięki czemu można zobaczyć pełny stan interfejsu użytkownika w jednym obrazie.

Użyj --capture-screen polecenia , aby przechwycić menu podręczne, listy rozwijane, wysuwane lub nakładki etykietek narzędzi. W --capture-screen trybie (i po ponowieniu próby po wykryciu pustej ramki) okno docelowe zostanie przeniesione na pierwszy plan; zwykłe przechwytywanie okien nie przenosi okna.

wywołać

Programowe aktywowanie elementu (kliknij przycisk, przełącz pole wyboru, rozwiń pole kombi).

winapp ui invoke btn-submit-7a90 -a myapp             # by slug from inspect
winapp ui invoke btn-submit-a1b2 -a myapp  # by slug from inspect/search
winapp ui invoke cmb-sizecombobox-b4c5 -a myapp # expand combo box

Próbuje wzorce w kolejności: InvokePattern → TogglePattern → SelectionItemPattern → ExpandCollapsePattern.

click

Kliknij element na jego ekranie współrzędnych przy użyciu symulacji myszy. Służy do kontrolek, które nie obsługują InvokePattern (np. nagłówki kolumn, elementy listy).

winapp ui click btn-column1-a3f2 -a myapp              # single click by slug
winapp ui click "Column1" -a myapp                      # single click by text search
winapp ui click btn-column1-a3f2 -a myapp --double      # double-click
winapp ui click btn-column1-a3f2 -a myapp --right       # right-click

set-value

Ustaw wartość na edytowalny element (tekst TextBox/ComboBox, liczba suwaka).

winapp ui set-value txt-textbox-a4b1 "Hello world" -a notepad
winapp ui set-value sld-volume-b2c3 75 -a myapp

get-value

Odczytaj bieżącą wartość z elementu. Używa inteligentnego łańcucha rezerwowego: TextPattern (RichEditBox, Document) → ValuePattern (TextBox, Slider) → SelectionPattern (ComboBox, RadioButton, TabView) → Name (etykiety).

winapp ui get-value doc-texteditor-53ad -a notepad          # read full document text
winapp ui get-value SearchBox -a myapp                      # read TextBox content
winapp ui get-value CmbTheme -a myapp                       # read ComboBox selected item
winapp ui get-value sld-volume-b2c3 -a myapp                # read Slider value
winapp ui get-value lbl-title-a1b2 -a myapp --json          # JSON: { "elementId": "...", "text": "..." }

focus

Przenieś fokus klawiatury do elementu.

winapp ui focus txt-textbox-a4b1 -a notepad

przewiń do widoku

Przewiń element do widocznego obszaru.

winapp ui scroll-into-view itm-targetitem-c3d4 -a myapp

wait-for

Poczekaj, aż element pojawi się, zniknie lub osiągnie wartość docelową.

winapp ui wait-for Button -a myapp --timeout 5000                       # wait for any button
winapp ui wait-for btn-submit-7a90 -a myapp --timeout 5000             # wait for specific element
winapp ui wait-for CounterDisplay -a myapp --value "5" --timeout 5000  # wait for element value (smart fallback)
winapp ui wait-for lbl-status -a myapp --property Name --value "Done" --timeout 5000  # wait for specific property
winapp ui wait-for btn-submit-a1b2 --gone -a myapp --timeout 2000      # wait for element to disappear
winapp ui wait-for lbl-status -a myapp --value "Done" --contains       # substring match instead of exact equality

Przewiń

Przewiń element kontenera. Znajdź kontenery search scroll z możliwością przewijania — poszukaj [scroll:v] (pionowych) lub [scroll:h] (poziomych) znaczników.

# Find which elements are scrollable and in which direction
winapp ui search scroll -a myapp
#   pn-scrollview-bfef Pane "scrollView" [scroll:v] (main content, vertical)
#   pn-scrollviewer-bfb1 Pane "scrollViewer" [scroll:h] (horizontal list)

# Scroll the main content down
winapp ui scroll pn-scrollview-bfef --direction down -a myapp

# Jump to top/bottom
winapp ui scroll pn-scrollview-bfef --to bottom -a myapp

# If you target an element that's not scrollable, scroll walks up to find the nearest scrollable parent
winapp ui scroll itm-someitem-a1b2 --direction down -a myapp

uzyskiwanie fokusu

Pokaż element, który obecnie ma fokus klawiatury.

winapp ui get-focused -a myapp

list-windows

Wyświetl listę wszystkich widocznych okien dla aplikacji, w tym wyskakujących okienek i okien dialogowych.

winapp ui list-windows -a imageresizer
winapp ui list-windows -a Terminal
winapp ui list-windows                                      # all windows (no filter)

Obsługa struktury

Framework Sprawdzić wyszukać wywołać set-value zrzut ekranu
WPF ✅ Pełne drzewo ✅ Wszystkie właściwości ✅ Wszystkie wzorce
Windows Forms
Win32
WinUI 3
Elektron ⚠✔ Drzewo chromowe ⚠✔ Ograniczone ⚠✔ Różni się ⚠✔ Różni się
Flutter ⚠✔ Podstawowa ⚠✔ Podstawowa ❌ Minimalne

Troubleshooting

Error Przyczyna Rozwiązanie
"Nie znaleziono uruchomionej aplikacji" Aplikacja nie działa lub niezgodność nazw Sprawdzanie nazwy procesu lub używanie identyfikatora PID
"Dopasowanie wielu okien" Niejednoznaczna -a wartość Użyj -w <HWND> z wymienionych opcji
"ma wiele okien" Proces ma wiele okien Użyj -w <HWND> polecenia , aby kierować do określonego elementu
"Selektor pasował do N elementów" Niejednoznaczny selektor starszej wersji Użyj slugs z inspect danych wyjściowych lub dołącz [0]element , [1] do starszych selektorów
"Element mógł ulec zmianie" Skrót Slug nie pasuje do bieżącego elementu inspect Uruchom ponownie lubsearch, aby uzyskać świeże ślimaki
"nie obsługuje żadnego wzorca wywołania" Nie można wywołać elementu Użyj inspect elementu w celu znalezienia elementu podrzędnego z możliwością wywołania
"Nie znaleziono okna UIA" Interfejs użytkownika nie widzi procesu Użyj list-windows polecenia , aby znaleźć HWND, a następnie -w
"Okno ma zerowy rozmiar" Okno jest zminimalizowane Aplikacja zostanie przywrócona automatycznie
Wyskakujące okienko/lista rozwijana nie są na zrzucie ekranu Funkcja PrintWindow nie przechwytuje nakładek Użyj --capture-screen flagi

Typowe wzorce

winapp ui invoke btn-settings-a1b2 -a myapp          # click a button
winapp ui wait-for pn-settingspage-c3d4 -a myapp    # wait for page to load
winapp ui screenshot -a myapp --output settings.png  # verify visually

Znajdowanie tekstu i wywoływanie jego elementu nadrzędnego

# Search shows invokable ancestor; invoke auto-walks to it
winapp ui invoke 'Save changes' -a myapp

# Or search first to see what matches, then invoke
winapp ui search "Save changes" -a myapp; winapp ui invoke btn-save-c3d4 -a myapp

Uściślanie zduplikowanych elementów

winapp ui search '#Image' -a myapp; winapp ui invoke itm-image-a2b3 -a myapp

Zrzut ekranu przedstawiający wyskakujące nakładki

winapp ui set-value txt-searchbox-e5f6 "query" -a myapp; winapp ui screenshot -a myapp --capture-screen
winapp ui invoke btn-settings-a1b2 -a myapp; winapp ui wait-for pn-settingspage-c3d4 -a myapp --timeout 3000; winapp ui screenshot -a myapp -o settings.png

Odnajdywanie, klikanie i weryfikowanie

winapp ui inspect -a myapp --interactive; winapp ui invoke btn-submit-7a90 -a myapp; winapp ui screenshot -a myapp

Interakcja okna dialogowego pliku

Okna dialogowe otwierania/zapisywania plików to standardowe okna dialogowe Windows z obsługą interfejsu użytkownika:

# Trigger the dialog, find it, type the path, confirm
winapp ui invoke btn-openfilebtn-a2b3 -a myapp
winapp ui list-windows -a myapp                                      # find dialog HWND
winapp ui set-value txt-1148-c4d5 "C:\path\to\file.png" -w <dialog-hwnd>
winapp ui invoke btn-open-e6f7 -w <dialog-hwnd>

Użyj inspect -w <dialog-hwnd> --interactive polecenia , aby odnaleźć rzeczywiste ślimaki dla określonego okna dialogowego.

Dlaczego ; łańcuch (nie &&)

Operator programu PowerShell może blokować się, gdy natywny && interfejs wiersza polecenia zapisuje w programie stderr lub używa sekwencji ucieczki ANSI. Zamiast tego — ; uruchamia każde polecenie bezwarunkowo i unika tego zakleszczenia. Jest to również lepsze w przypadku przepływów pracy agenta: zwykle chcesz, aby zrzut ekranu był uruchamiany nawet wtedy, gdy wywołanie miało wyjście niezerowe.

Wzorce testowania ciągłej integracji

Użyj winapp ui poleceń w potokach ciągłej integracji (GitHub Actions, Azure DevOps) na potrzeby testów weryfikacyjnych kompilacji i weryfikacji interfejsu użytkownika. wait-for z --property i --value działa jako asercja — zwraca kod zakończenia 1 w przypadku przekroczenia limitu czasu, co powoduje automatyczne niepowodzenie kroku ciągłej integracji.

Uruchamianie i testowanie w GitHub Actions

steps:
  - name: Build
    run: dotnet build MyApp.csproj -c Debug -p:Platform=x64

  - name: Launch and test
    run: |
      $result = winapp run .\bin\x64\Debug\net8.0-windows10.0.26100.0\win-x64 --detach --json | ConvertFrom-Json
      $appPid = $result.ProcessId

      # Wait for window to initialize
      winapp ui wait-for "Main Window" -a $appPid --timeout 30000

      # Run tests — each wait-for exits non-zero on failure
      winapp ui invoke "Login" -a $appPid
      winapp ui wait-for "Dashboard" -a $appPid --timeout 10000
      winapp ui screenshot -a $appPid -o dashboard.png

Stan elementu assert z wait-for

wait-for --value sonduje, dopóki wartość elementu nie pasuje do oczekiwanego ciągu, używając tego samego inteligentnego rezerwowego co get-value (TextPattern → ValuePattern → SelectionPattern → Name). Zwraca kod zakończenia 0 zgodny, kod zakończenia 1 w przypadku przekroczenia limitu czasu — co czyni go asercją przyjazną dla ciągłej integracji. Zamiast tego użyj polecenia --property , aby sprawdzić określoną właściwość UIA.

# Assert: button click updated the counter (smart value fallback — works for TextBlock, TextBox, etc.)
winapp ui invoke "Counter Button" -a $pid
winapp ui wait-for "Counter Display" -a $pid --value "Count: 1" -t 5000

# Assert: text input was accepted
winapp ui set-value "Search Box" "hello world" -a $pid
winapp ui wait-for "Search Box" -a $pid --value "hello world" -t 3000

# Assert: checkbox was toggled (use --property for specific UIA properties)
winapp ui invoke "Dark Mode" -a $pid
winapp ui wait-for "Dark Mode" -a $pid --property ToggleState --value "On" -t 3000

# Assert: navigation happened (new page appeared)
winapp ui invoke "Settings" -a $pid
winapp ui wait-for "Settings Page" -a $pid -t 10000

# Assert: dialog was dismissed (element disappeared)
winapp ui invoke "Close" -a $pid
winapp ui wait-for "Dialog Title" -a $pid --gone -t 5000

Potwierdzanie przy użyciu danych wyjściowych JSON

Używanie z --json programem PowerShell lub jq w celu uzyskania bardziej złożonych asercji:

Kontrakt zakończenia kodu dla search i wait-for w --json trybie: gdy żaden element nie pasuje (search) lub limit czasu oczekiwania (wait-for), polecenie zapisuje w pełni analizowalne koperty wyniku do stdout ({ "matchCount": 0, ... } lub { "found": false, "timedOut": true, ... }) i zwraca kod zakończenia 1. Stderr jest pusty w --json trybie (dane wyjściowe rejestratora są pomijane). Rozgałęzij na polach koperty lub na $LASTEXITCODE, w zależności od tego, co jest bardziej ergonomiczne.

# Assert: search found exactly one match
$result = winapp ui search "Submit" -a $pid --json | ConvertFrom-Json
if ($result.matchCount -ne 1) { throw "Expected 1 Submit button, found $($result.matchCount)" }

# Assert: element has expected properties
# inspect --json returns { windows: [{ hwnd, title, elements: [...] }] };
# each window's elements[] is the nested tree (children rendered via .children).
$tree = winapp ui inspect "Counter Display" -a $pid --json | ConvertFrom-Json
$counter = $tree.windows[0].elements[0]
if ($counter.name -ne "Count: 3") { throw "Counter value wrong: $($counter.name)" }

Przykład pełnego testu weryfikacyjnego kompilacji

# Launch
$app = winapp run .\build-output --detach --json | ConvertFrom-Json

# Verify app loaded
winapp ui wait-for "Main Page" -a $app.ProcessId -t 30000

# Interact and assert
winapp ui invoke "Add Item" -a $app.ProcessId
winapp ui set-value "Item Name" "Test Item" -a $app.ProcessId
winapp ui invoke "Save" -a $app.ProcessId
winapp ui wait-for "Test Item" -a $app.ProcessId -t 5000              # assert item appeared in list
winapp ui wait-for "Save" -a $app.ProcessId --gone -t 3000            # assert save dialog closed

# Visual verification
winapp ui screenshot -a $app.ProcessId -o smoke-test.png