mcpField
W
winui-ui-testing

winui-ui-testing

github.com/microsoftテスト・品質490201 回インストール3 週間前に更新
リポジトリ

winui-ui-testing について

Windows UI AutomationとWinApp CLIでデスクトップアプリを検査・操作するテストスキルです。ローカルまたはWindows Sandboxの指定、スクリプトによるアサーション、取得した証跡の目視確認を扱います。

主な機能

  1. 01UI Automationツリーの検査と検索
  2. 02入力やコントロール操作による対話
  3. 03ローカルまたはWindows Sandboxでの実行
  4. 04値とアプリ状態のアサーション
  5. 05スクリーンショットと録画の取得
  6. 06UIAでは分からない表示状態の確認

活用シーン

  1. 01スクリプトアサーションによるデスクトップ操作のテスト
  2. 02ダイアログ、ピッカー、フライアウトの検査
  3. 03目視確認用の画面状態の記録
  4. 04Windows SandboxでのUIテスト実行

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/microsoft/win-dev-skills --skill winui-ui-testing

インストール後、Claude Code で/winui-ui-testingを入力して読み込まれたか確認します。

詳細

対象範囲と実行境界

winapp ui は Windows UI Automation(UIA)を使用するため、AutomationId ベースのワークフローは、パッケージ化の有無を問わず、あらゆる Windows デスクトップフレームワークで動作します。他のフレームワークでは、WinUI 固有のバインディングやダイアログに関するアドバイスは読み飛ばしてください。

Windows Sandbox は合成入力がユーザーのデスクトップに届かないようにします。対象の選択はステップ 1 で行います。winapp run --help、winapp target --help、winapp ui <verb> --help を実行して、インストール済みの仕様を確認してください。--sandbox やトップレベルの sandbox コマンドではなく、--on sandbox を使用してください。タスクの許可なしに Windows の機能を有効にしたり、アプリを起動したりしないでください。

  • 前提条件と有効化については、winui-setup を参照してください。
  • プロジェクトのビルド/発行はホストで実行し、デプロイ、アプリの起動、ui --on sandbox はゲストで実行します。以下のバッチスクリプトはホストで実行してください。target exec 内では実行しないでください(ルーティングが二重になります)。
  • 実入力とキャプチャには、ロック解除されたホストと、接続済みで最小化されていない Sandbox クライアントが必要です。入力できなくてもツリーの調査はできる場合があります。ツリーを読み取れることは、入力可能な状態かどうかの確認にはなりません。
  • winapp target snapshot sandbox --json は読み取り専用の準備状態クエリであり、ゲストを起動したり再接続したりしません。ユーザーのデスクトップを調べて確認するのではなく、準備状態の診断に使用してください。
  • 永続的なゲストは共有されており、相互に信頼できないワークフロー間を隔離するものではありません。他のユーザーと調整してください。サポート対象の --unique-identity オプションはありません。相互に信頼できない作業には、別々のマシンを使用してください。
  • ゲストの --debug-output がサポートされるのは、パッケージ化されたアプリのみです。
アプローチとコマンドの確認

対話型の呼び出しを長々と繰り返すより、スクリプト化したバッチを 1 回実行する方法を優先してください。アプリを自分で作成した場合は、XAML/ソースの AutomationId を直接使用してください。それ以外の場合は、現在のツリーを調べ、非表示/遅延表示されるフライアウトやダイアログについてソースを確認してください。どちらの場合も、アクセシビリティ監査に合格と判断する前に、実際のウィンドウ/ツリーを検証してください。

主な動詞:list-windows、inspect、search、get-property、get-value、wait-for、invoke、click、set-value、focus、scroll-into-view、send-keys、hover、drag、touch、pen、screenshot、record。セレクターやオプションは、この短いリファレンスから推測せず、各動詞の --help を確認してください。

ステップ 1:対象を選択し、PID と対象を常にセットで扱う

Windows Sandbox が利用可能な場合は sandbox を優先してください。利用できない場合はユーザーに伝えて local を選択します。ユーザーが Windows Sandbox を明示的に要求し、利用できない場合は、処理を中止して winui-setup の有効化手順を案内してください。ゲストが停止していることは、利用できない証拠にはなりません。機能が有効でも、target snapshot は実行中の対象がないと報告することがあります。アプリのビルド/テストの失敗は、Sandbox が利用できないことを意味しません。同じ方針は診断にも適用されます。パッケージ化されていないアプリではゲストの --debug-output を使用できないため、Sandbox が明示的に要求されていない限り、ローカルで診断してください。

選択した対象をテンプレートに渡します。ゲストの場合は winapp run . --on sandbox --detach --json で起動し、ローカル実行の場合は --on sandbox を省略します。キャプチャした対象が選択した対象と一致し、ゲストが再作成されていない場合に限り、すでに実行中のアプリを再利用してください。ゲストの PID を、対象を指定しないホスト側の winapp ui に渡してはいけません。選択後に対象を利用できなくなった場合は、その旨を報告し、同じ方針に従って対象を選び直してください。スクリプト自体は別の対象で再試行しません。

実行時の JSON には ProcessId、ProcessScope、UiTargetArgs(PascalCase)が含まれます。Windows Sandbox では、UiTargetArgs は --on sandbox -a <guestPID> です。数値だけでなく、両方の部分を保持してください。パッケージ化されていないゲスト実行を明示的に要求した場合、起動結果にアプリの PID は含まれません。別途起動し、対象全体を指定した winapp ui list-windows --on sandbox --json でプロセス名/タイトルから目的のアプリを見つけ、その PID を -Target sandbox -AppPid <PID> -AppScope sandbox で渡してください。コンテナー化の PID は使用しないでください。新しいゲストでは、以前の PID/HWND は無効になります。

Step 2: ホスト側のバッチを作成する

ui-tests.ps1 を作成し、サンプルの AutomationIds/期待値をアプリの要件に合わせて置き換え、以下の関連する例を外側の try 内、finally の前に追加します。2つの小さなコマンドヘルパーは CLI 呼び出しの振り分けと確認のみを行い、ターゲットマネージャーを実装するものではありません。後続の成功によって先行するネイティブエラーが隠れないよう、追加するすべてのコマンドでこれらを使用してください。

# ui-tests.ps1
param(
    [Parameter(Mandatory)][ValidateSet('sandbox', 'local')][string]$Target,
    [int]$AppPid = 0,
    [ValidateSet('sandbox', 'local')][string]$AppScope,
    [ValidateNotNullOrEmpty()][string]$WorkflowId = [guid]::NewGuid().ToString('N'),
    [string]$ArtifactDirectory = (Join-Path '.\ui-test-artifacts' ([guid]::NewGuid().ToString('N')))
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
$ScopeArgs = @(if ($Target -eq 'sandbox') { '--on'; 'sandbox' })
$previousWorkflowId = $env:WINAPP_UI_WORKFLOW_ID
$env:WINAPP_UI_WORKFLOW_ID = $WorkflowId
$results = [Collections.Generic.List[object]]::new()
$screenshots = [Collections.Generic.List[string]]::new()
$uiStarted = $false

function Invoke-WinAppChecked {
    param([string[]]$Arguments)
    $global:LASTEXITCODE = $null
    $output = @(& winapp @Arguments)
    $code = $global:LASTEXITCODE
    if ($null -eq $code -or $code -ne 0) {
        throw "winapp $($Arguments -join ' ') failed (exit $code). $($output -join "`n")"
    }
    $output
}

function Invoke-Ui {
    param([string[]]$Arguments, [string]$Window)
    $selector = if ($Window) { @('-w', $Window) } else { @('-a', "$AppPid") }
    Invoke-WinAppChecked (@('ui') + $Arguments + $ScopeArgs + $selector)
}

function Test-UI {
    param([string]$Name, [scriptblock]$Script)
    try {
        & $Script | Out-Null
        $results.Add(@{ name = $Name; status = 'PASS' })
    } catch {
        $results.Add(@{ name = $Name; status = 'FAIL'; detail = "$_" })
    }
}

function Save-Screenshot {
    param([string]$Name)
    $path = Join-Path $ArtifactDirectory $Name
    if (Test-Path -LiteralPath $path) { throw "Choose a new evidence path: $path" }
    Invoke-Ui @('screenshot', '--output', $path) -Window $hwnd | Out-Null
    if (-not (Test-Path -LiteralPath $path -PathType Leaf) -or (Get-Item -LiteralPath $path).Length -eq 0) {
        throw "Screenshot was not delivered to the host: $path"
    }
    $screenshots.Add($path)
}

try {
    $ArtifactDirectory = [IO.Path]::GetFullPath($ArtifactDirectory)
    New-Item -ItemType Directory -Force -Path $ArtifactDirectory | Out-Null
    if ($AppPid -lt 0) { throw 'AppPid must be positive, or zero to launch.' }
    if ($AppPid -gt 0) {
        if ($AppScope -ne $Target) {
            throw 'Reusing a PID requires matching -AppScope and -Target from the same live target.'
        }
    } else {
        $launch = Invoke-WinAppChecked (@('run', '.') + $ScopeArgs + @('--detach', '--json')) |
            ConvertFrom-Json
        if ($launch.Error -or -not $launch.ProcessId -or [int]$launch.ProcessId -le 0) {
            throw "No app ProcessId. For unpackaged guest runs, discover the app in that target and rerun with -AppPid and -AppScope. $($launch.Error)"
        }
        $AppPid = [int]$launch.ProcessId
        if ($Target -eq 'sandbox' -and
            ($launch.ProcessScope -ne 'sandbox' -or $launch.UiTargetArgs -ne "--on sandbox -a $AppPid")) {
            throw 'Launch response is missing the expected Sandbox ProcessScope / UiTargetArgs pair.'
        }
        if ($Target -eq 'local' -and $launch.ProcessScope -and $launch.ProcessScope -ne 'local') {
            throw 'Launch response belongs to a different target.'
        }
    }

    $uiStarted = $true
    $windows = @(Invoke-Ui @('list-windows', '--json') | ConvertFrom-Json)
    $main = $windows | Where-Object { $_.hwnd -and $_.hwnd -ne '0' -and $_.title -ne 'PopupHost' } |
        Select-Object -First 1
    if (-not $main) { throw 'No app window found in the selected target; rediscover after a fresh guest.' }
    $hwnd = [string]$main.hwnd

    Test-UI 'Initial screenshot delivered' { Save-Screenshot '01-initial.png' }
    Test-UI 'Home exists' { Invoke-Ui @('wait-for', 'NavHome', '-t', '3000') }
    Test-UI 'Navigate to Settings' {
        Invoke-Ui @('invoke', 'NavSettings')
        Invoke-Ui @('wait-for', 'TxtUserName', '-t', '3000')
    }
    Test-UI 'Save username' {
        Invoke-Ui @('set-value', 'TxtUserName', 'TestUser')
        Invoke-Ui @('invoke', 'BtnSave')
        Invoke-Ui @('wait-for', 'TxtUserName', '--value', 'TestUser', '-t', '2000')
    }
    Test-UI 'Default values' {
        Invoke-Ui @('wait-for', 'CmbTheme', '--value', 'System default', '-t', '2000')
        Invoke-Ui @('wait-for', 'TglLogging', '--value', 'Off', '-t', '2000')
    }
    Test-UI 'App controls have AutomationIds' {
        $inspection = Invoke-Ui @('inspect', '--interactive', '--json') -Window $hwnd | ConvertFrom-Json
        function Get-Elements($nodes) {
            foreach ($node in $nodes) {
                if ($null -ne $node) { $node; Get-Elements $node.children }
            }
        }
        $allElements = @(Get-Elements @($inspection.windows | ForEach-Object { $_.elements }))
        if (-not $allElements.Count) { throw 'Inspection returned no elements; not an accessibility PASS.' }
        $appElements = @($allElements | Where-Object {
            $_.type -match 'Button|TextBox|ComboBox|CheckBox|ToggleSwitch|TabItem|Edit' -and
            $_.name -notmatch 'Minimize|Maximize|Close|System' -and
            $_.className -notmatch 'PickerHost|#32770|CabinetWClass'
        })
        if (-not $appElements.Count) { throw 'No app controls were audited; adjust selectors rather than passing.' }
        $missingId = @($appElements | Where-Object { -not $_.automationId })
        if ($missingId.Count) {
            throw "Missing AutomationIds: $(($missingId | ForEach-Object { "$($_.type) '$($_.name)'" }) -join ', ')"
        }
    }
    Test-UI 'Final screenshot delivered' { Save-Screenshot '02-settings.png' }
} catch {
    $results.Add(@{ name = 'Target setup / test execution'; status = 'FAIL'; detail = "$_" })
} finally {
    try {
        if ($uiStarted) { Invoke-WinAppChecked (@('ui', 'yield') + $ScopeArgs) | Out-Null }
    } catch {
        $results.Add(@{ name = 'Release UI workflow'; status = 'FAIL'; detail = "$_" })
    } finally {
        $env:WINAPP_UI_WORKFLOW_ID = $previousWorkflowId
    }
}

$failed = @($results | Where-Object { $_.status -eq 'FAIL' }).Count
$report = [ordered]@{
    target = $Target; appPid = $AppPid; workflowId = $WorkflowId
    passed = $results.Count - $failed; failed = $failed
    results = @($results.ToArray()); screenshots = @($screenshots.ToArray())
}
$report | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath '.\test-results.json' -Encoding utf8
Write-Host "Passed: $($report.passed) | Failed: $failed | Results: .\test-results.json"
if ($failed) { exit 1 }
exit 0

テスト内では exit ではなく throw を使用してください。スクリプトは CLI エラー、空の検査結果、証拠が配信されなかった場合に失敗します。スクリーンショットは既知のメイン HWND を対象にすることで、ホスト側の毎回同じパスに保存されます。ポップアップを含める必要がある場合は --capture-screen を使用してください。

Step 3: 実行し、結果を読み、目視で確認する
# Preferred when Windows Sandbox is available:
.\ui-tests.ps1 -Target sandbox
if ($LASTEXITCODE -ne 0) { throw 'UI batch failed; read test-results.json.' }
$report = Get-Content -LiteralPath '.\test-results.json' -Raw | ConvertFrom-Json
$report.screenshots

キャプチャ済みで、まだ実行中のゲスト PID がある場合は、.\ui-tests.ps1 -Target sandbox -AppPid 1234 -AppScope sandbox を使用してください。ローカルで実行する場合(Step 1 を参照)は、.\ui-tests.ps1 -Target local を使用し、必要に応じてローカル PID と -AppScope local を指定してください。ゲスト PID をローカルで再利用したり、テストの失敗を解消するためにターゲットを切り替えたりしないでください。

test-results.json に記載された各ホスト PNGを、画像表示ツールで確認してください。UIA の PASS では、クリッピング、重なり、テーマの誤り、コンテナーからはみ出すコンテンツを検出できません。意図しないスクロールバーや省略記号、クリップされたヒーロー領域/右端のコントロール、行の重なり、余白の不均衡、窮屈または広すぎる間隔、Light/Dark/High Contrast の誤り、フォーカス/ホバー/エラー状態の欠落があれば、目視確認を不合格としてください。意味のある状態は、最後だけでなく、操作の直後にキャプチャしてください。

修正を依頼された場合は、失敗項目をまとめて修正し、同じターゲットでテンプレートを使って再ビルド/再起動し(再起動する場合は -AppPid を省略)、新しい PID をキャプチャしてください。修正と再実行は最大 2 サイクルです。その後も残る失敗を報告してください。構造化された結果と目視確認の両方が完了するまで、完了を宣言しないでください。

アサーションとカバレッジ

依頼されたすべての要件についてテストを作成してください。TextBox/NumberBox の値、RichEditBox のテキスト、ComboBox の選択、トグル/チェックボックスの On/Off、TextBlock/ラベルのテキストには wait-for --value を使用してください。--contains は部分文字列に一致し、--gone は消えるまで待機します。-p IsEnabled --value True は特定のプロパティに対してのみ使用してください。ボタン/ナビゲーション項目を呼び出し、ページ固有の要素が表示されるまで待機し、既定/エラー/空の状態をテストしてください。

以下の例はテンプレートへの追加であり、独立したシェルではありません。Invoke-Ui は常に、選択したスコープと -a $AppPid、またはスコープと -w $Window を指定します。生の値を読み取る場合は --json を渡して解析してください。標準出力には補足メッセージが含まれることがあります。折りたたみより下にある仮想化項目をアサートする前に、scroll-into-view を使用してください。

Test-UI 'Status and enabled state' {
    Invoke-Ui @('wait-for', 'StatusBar', '--value', 'saved', '--contains', '-t', '3000')
    Invoke-Ui @('wait-for', 'BtnSave', '-p', 'IsEnabled', '--value', 'True', '-t', '3000')
    $value = Invoke-Ui @('get-value', 'TxtUserName', '--json') | ConvertFrom-Json
    if ($value.text -ne 'TestUser') { throw 'Wrong username.' }
}
ファイルピッカー、フライアウト、ContentDialog

ピッカーは別の PickerHost プロセスで実行されます。同じターゲット内で HWND を検出してください。-w はウィンドウセレクターを変更しますが、ターゲットは変更しません。Sandbox テストでは、ファイル名をゲスト内に存在するファイルに置き換えてください。

Test-UI 'Open file picker' {
    Invoke-Ui @('invoke', 'BtnOpenFile')
    Start-Sleep -Seconds 1
    $allWindows = @(Invoke-WinAppChecked (@('ui', 'list-windows', '--json') + $ScopeArgs) | ConvertFrom-Json)
    $pickers = @($allWindows | Where-Object { $_.hwnd -and [string]$_.ownerHwnd -eq $hwnd -and $_.title -match 'Open|Save' })
    if ($pickers.Count -ne 1) { throw 'Expected one picker; inspect the selected target again.' }
    $pickerHwnd = [string]$pickers[0].hwnd
    Invoke-Ui @('inspect', '--interactive', '--json') -Window $pickerHwnd
    Invoke-Ui @('set-value', 'FileNameControlHost', 'test.txt') -Window $pickerHwnd
    Invoke-Ui @('invoke', 'Open') -Window $pickerHwnd
    Invoke-Ui @('wait-for', 'StatusBar', '--value', 'opened', '--contains', '-t', '3000')
}

Test-UI 'Copy context menu' {
    Invoke-Ui @('click', 'LstItems', '--right')
    Invoke-Ui @('wait-for', 'MnuCopy', '-t', '2000')
    Invoke-Ui @('invoke', 'MnuCopy')
    Invoke-Ui @('wait-for', 'StatusText', '--value', 'Copied', '-t', '2000')
}

Test-UI 'Confirm deletion' {
    Invoke-Ui @('invoke', 'BtnDelete')
    Invoke-Ui @('wait-for', 'Primary', '-t', '2000')
    Invoke-Ui @('invoke', 'Primary')
    Invoke-Ui @('wait-for', 'Primary', '--gone', '-t', '3000')
}

保存/キャンセル用ピッカーでは、適切なアクションとアサーションに置き換えてください。MenuBar のヘッダーとフライアウト項目も同様に invoke と wait-for を使用します。ContentDialog はアプリのウィンドウ内にあります。Primary、Secondary、Close は一般的なセレクターですが、カスタム AutomationIds が保証されていないため、実際に開いているダイアログを確認してください。

キーボード、ホバー、ドラッグ、タッチ、ペン

WinUI/WPF のテキストコントロールでアクセラレーターと信頼性の高い文字単位の入力を行うには、send-keys --via send-input が必要です。post-message は HWND を対象にしますが、文字ごとの KeyDown は発生しません。--target は最初にフォーカスを設定し、--verbatim はキー名を文字どおりに入力します。システムショートカットには、CLI の明示的な許可フラグが必要です。デスクトップ準備完了エラーを回避する手段として使用しないでください。

Test-UI 'Keyboard save' {
    Invoke-Ui @('send-keys', 'hello world', '--target', 'TxtName', '--via', 'send-input')
    Invoke-Ui @('send-keys', 'ctrl+s', '--via', 'send-input')
    Invoke-Ui @('wait-for', 'StatusBar', '--value', 'saved', '--contains', '-t', '2000')
}
Test-UI 'Tooltip' {
    Invoke-Ui @('hover', 'BtnInfo')
    Invoke-Ui @('wait-for', 'InfoTooltip', '-t', '2000')
}
Test-UI 'Reorder items' {
    Invoke-Ui @('drag', 'ItemA', 'ItemB')
    Invoke-Ui @('wait-for', 'StatusBar', '--value', 'reordered', '--contains', '-t', '2000')
}
Test-UI 'Touch and ink input' {
    Invoke-Ui @('touch', 'LstFeed', '-g', 'swipe', '--direction', 'up', '--distance', '400')
    Invoke-Ui @('pen', 'InkCanvas', '--path', '50,50 120,80 200,60', '--pressure', '0.8')
    Save-Screenshot '03-ink.png'
    # Add the app-specific state assertion; successful injection alone is not functional proof.
}

座標、長押し/滞留、ピンチ/ストレッチ、圧力/傾き/消しゴムのサポートについては、winapp ui drag --help、winapp ui touch --help、winapp ui pen --help を使用してください。座標はゲストのツリーで確認してください。ホストの座標はゲストの座標ではありません。

録画とワークフローの調整
Test-UI 'Video delivered' {
    $video = Join-Path $ArtifactDirectory 'flow.mp4'
    if (Test-Path -LiteralPath $video) { throw 'Choose a new video evidence path.' }
    Invoke-Ui @('record', '--duration-sec', '6', '--fps', '30', '--output', $video)
    if (-not (Test-Path -LiteralPath $video -PathType Leaf) -or (Get-Item -LiteralPath $video).Length -eq 0) {
        throw "Recording was not delivered to the host: $video"
    }
}

Sandbox のスクリーンショット/録画で指定する --output のパスはホスト側のパスで、キャプチャ完了時に自動的に配信されます。推測したゲスト側のディレクトリから取得しないでください。新しい出力パスを使用し、終了ステータスと実際のホスト側のファイルを確認してください。--capture-screen では、アプリウィンドウの外側にあるポップアップも含まれる場合があります。録画が中断された場合は、stopReason、partialOutput、recoveryHint、および報告された部分出力/復旧用パスを保持してください。部分ファイルが空でなくても、再生できるとは限りません。

WinApp CLI は、デスクトップを変更するコマンドを自動的に調整します。ワークフロー ID がない場合、各コマンドは実行直後に順番を譲ります。ワークフロー ID がある場合、4 秒間の猶予は短時間に連続するスクリプトの実行を想定したものであり、エージェントが推論する時間を含むものではありません。録画と操作を同時に行う場合も含め、連携するコマンドでは同じ WINAPP_UI_WORKFLOW_IDを使用してください。独立したフローには異なる ID が必要です。このテンプレートでは、この目的のために -WorkflowId を受け取り、独自のプロセス内で ID を設定して、以前の値に戻し、finally で winapp ui yield --on sandbox を呼び出します。新しいシェルツールの呼び出しには、以前のシェルでの変更は引き継がれません。連携するすべての呼び出しで同じ ID を設定してください。別プロセスで並行録画する場合も、終了コードと配信を確認し、同じスコープの PID を使用して、テストランナーが順番を譲る前に終了させる必要があります。録画対象の操作を妨げる、独立した ID の録画を開始しないでください。一時停止した後は、もう一度状態を確認し、別のワークフローが変更した可能性のあるメニューやダイアログを開き直してください。

ゲスト側の永続化と任意のファイル転送

ゲスト側での永続化を証明するために、ホスト側の $env:LOCALAPPDATA を調べないでください。選択したターゲット内にある実際のアプリデータファイルを確認してください。サンプルのパッケージファミリー/パスは、アプリの実際の保存場所に置き換えてください。通常、パッケージ化されていないアプリは、アプリ固有の LocalAppData ディレクトリを使用します。

Test-UI 'Username persisted in selected target' {
    $readSettings = @'
$ErrorActionPreference = 'Stop'
$file = Join-Path $env:LOCALAPPDATA 'Packages\YourPackageFamily\LocalState\settings.json'
if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "Missing settings: $file" }
Get-Content -LiteralPath $file -Raw
'@
    $json = if ($Target -eq 'sandbox') {
        Invoke-WinAppChecked @('target', 'exec', 'sandbox', '--', 'powershell.exe', '-NoProfile', '-NonInteractive', '-Command', $readSettings)
    } else {
        & ([scriptblock]::Create($readSettings))
    }
    $settings = $json | ConvertFrom-Json
    if ($settings.UserName -ne 'TestUser') { throw 'Saved username does not match.' }
}

任意のセットアップ/結果ファイルには、ホスト側のファイルシステムから推測するのではなく、target exec / push / pull を使用してください。転送するゲスト側のパスは絶対パスではなく、スナップショットの workRoot を基準とした相対パスです。通常は C:\WinApp\work です。以下のオプションの追加手順は、ゲストの作業ルート内に Results を作成するホスト側の setup.ps1 があることを前提としています。

Test-UI 'Guest setup and result transfer' {
    if ($Target -ne 'sandbox') { throw 'This transfer test requires Sandbox.' }
    $snapshot = Invoke-WinAppChecked @('target', 'snapshot', 'sandbox', '--json') | ConvertFrom-Json
    if (-not $snapshot.workRoot) { throw 'No live guest workRoot; fix readiness first.' }
    Invoke-WinAppChecked @('target', 'push', 'sandbox', '.\setup.ps1', 'Setup\setup.ps1')
    $guestSetup = $snapshot.workRoot.TrimEnd('\') + '\Setup\setup.ps1'
    Invoke-WinAppChecked @('target', 'exec', 'sandbox', '--', 'powershell.exe', '-NoProfile', '-File', $guestSetup)
    Invoke-WinAppChecked @('target', 'pull', 'sandbox', 'Results', (Join-Path $ArtifactDirectory 'results'))
}

配信失敗から復旧する間は、ゲストを起動したままにしてください。診断に必要な証拠/復旧用パスとアプリデータを保持してから、ユーザーの同意を得たシャットダウンを行ってください。通常のクリーンアップとして、すべての Sandboxes を停止しないでください。

バインディングとセレクターに関する注意点
  • set-value は、フォーカスが変わるまで、既定の WinUI x:Bind TwoWay TextBox バインディングをコミットしません。アプリ側で UpdateSourceTrigger=PropertyChanged を使うことを推奨します。それができない場合は、永続化を確認する前に invoke で Save を実行するか、別のコントロールに focus を移してください。
  • RichEditBox/RichTextBox では、通常、UIA set-value ではなく send-keys --via send-input が必要です。
  • PowerShell の読み取り専用自動変数 $Pid ではなく、$AppPid を使用してください。
  • ピッカーの HWND には -w と同じターゲットスコープが必要です。新しいゲストでは HWND と PID の両方が無効になります。どちらも再利用しないでください。

コメント