Files
charleswiltgen__axiom/tools/xcui/cmd_dialog_test.go
T
Charles Wiltgen e996de9d79 fix(xcui): send a physical tap, and refuse to guess the simulator
AXe's default tap style sends a simulator tap that SwiftUI controls
ignore while AXe still prints a success line. Measured on Xcode 27.1
with AXe 1.8.0, across iPhone 17 (iOS 27.0) and iPhone Duo (iOS 27.1):
neither a Button, a List row, a Button inside a List, a Menu, a row of
an open Menu nor a TabView tab activated under the default or the
`simulator` style, and `--tap-style physical` activated all six. The
same default left system alerts on screen while `dialog accept`
reported them handled.

`tap` and the `dialog` accept/dismiss taps now supply the physical
style unless the caller picks one, and an AXe older than 1.7.0 — which
has no `--tap-style` — is named as the cause instead of surfacing as an
unknown-flag failure.

With more than one simulator booted and no `--udid`, every device verb
now exits 2 and lists each booted device's UDID, name and runtime,
rather than driving whichever sorted first while reporting success.
`doctor` reports that list, fails its gate in that state, and leaves
`booted_udid` empty, because nothing may be targeted until the caller
chooses.
2026-09-19 15:43:23 -07:00

186 lines
6.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"reflect"
"testing"
)
// Argument-order independence (axiom-v9in): pregrant must accept --udid before
// or after its positionals and yield identical results. This is xcui's per-tool
// regression guard for the cross-tool standard.
func TestParsePregrantArgsOrderIndependent(t *testing.T) {
before := []string{"--udid", "ABC123", "com.example.app", "camera", "photos"}
after := []string{"com.example.app", "camera", "photos", "--udid", "ABC123"}
bb, sb, ub, _, cb := parsePregrantArgs(before)
ba, sa, ua, _, ca := parsePregrantArgs(after)
if cb != 0 || ca != 0 {
t.Fatalf("both orders must succeed: before=%d after=%d", cb, ca)
}
if bb != ba || ub != ua || !reflect.DeepEqual(sb, sa) {
t.Errorf("order changed result: before(bundle=%q udid=%q svcs=%v) != after(bundle=%q udid=%q svcs=%v)", bb, ub, sb, ba, ua, sa)
}
if ba != "com.example.app" || ua != "ABC123" || !reflect.DeepEqual(sa, []string{"camera", "photos"}) {
t.Errorf("unexpected parse: bundle=%q udid=%q svcs=%v", ba, ua, sa)
}
}
func TestParsePregrantArgsRequiresBundleAndService(t *testing.T) {
if _, _, _, _, code := parsePregrantArgs([]string{"com.example.app"}); code != 2 {
t.Errorf("expected usage error (2) with no service, got %d", code)
}
}
// permissionAlert is a camera-permission dialog: an alert container with the
// standard two-button "Don't Allow" / "Allow" layout (straight apostrophe).
const permissionAlert = `[
{
"AXUniqueId": null, "AXLabel": "App", "role": "AXApplication", "type": "Application",
"enabled": true, "frame": {"x":0,"y":0,"width":402,"height":874},
"children": [
{
"AXUniqueId": null, "AXLabel": "“App” Would Like to Access the Camera",
"role": "AXSheet", "type": "Alert", "enabled": true,
"frame": {"x":51,"y":337,"width":300,"height":200},
"children": [
{"AXUniqueId": null, "AXLabel": "Don't Allow", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":51,"y":480,"width":150,"height":44}, "children": []},
{"AXUniqueId": "allow.btn", "AXLabel": "Allow", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":201,"y":480,"width":150,"height":44}, "children": []}
]
}
]
}
]`
// locationAlert offers the three-way location choice; accept must prefer the
// most-permissive standard label that is present.
const locationAlert = `[
{"AXUniqueId": null, "role": "AXSheet", "type": "Alert", "enabled": true,
"frame": {"x":51,"y":337,"width":300,"height":260}, "children": [
{"AXUniqueId": null, "AXLabel": "Allow Once", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":51,"y":440,"width":300,"height":44}, "children": []},
{"AXUniqueId": null, "AXLabel": "Allow While Using App", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":51,"y":490,"width":300,"height":44}, "children": []},
{"AXUniqueId": null, "AXLabel": "Dont Allow", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":51,"y":540,"width":300,"height":44}, "children": []}
]}
]`
// okAlert is a single-button informational alert.
const okAlert = `[
{"AXUniqueId": null, "role": "AXSheet", "type": "Alert", "enabled": true,
"frame": {"x":51,"y":337,"width":300,"height":150}, "children": [
{"AXUniqueId": null, "AXLabel": "OK", "role": "AXButton", "type": "Button",
"enabled": true, "frame": {"x":51,"y":440,"width":300,"height":44}, "children": []}
]}
]`
func TestFindAlertButtonAccept(t *testing.T) {
roots, _ := parseDescribeUI([]byte(permissionAlert))
btn, ok := findAlertButton(roots, intentAccept)
if !ok {
t.Fatal("expected to find an accept button")
}
if got := deref(btn.AXLabel); got != "Allow" {
t.Errorf("accept button = %q, want %q", got, "Allow")
}
}
func TestFindAlertButtonDismiss(t *testing.T) {
roots, _ := parseDescribeUI([]byte(permissionAlert))
btn, ok := findAlertButton(roots, intentDismiss)
if !ok {
t.Fatal("expected to find a dismiss button")
}
if got := deref(btn.AXLabel); got != "Don't Allow" {
t.Errorf("dismiss button = %q, want %q", got, "Don't Allow")
}
}
func TestFindAlertButtonDismissCurlyApostrophe(t *testing.T) {
roots, _ := parseDescribeUI([]byte(locationAlert))
btn, ok := findAlertButton(roots, intentDismiss)
if !ok {
t.Fatal("expected to match Dont Allow with a curly apostrophe")
}
if got := deref(btn.AXLabel); got != "Dont Allow" {
t.Errorf("dismiss button = %q, want the curly-apostrophe variant", got)
}
}
func TestFindAlertButtonAcceptPrefersMostPermissive(t *testing.T) {
roots, _ := parseDescribeUI([]byte(locationAlert))
btn, _ := findAlertButton(roots, intentAccept)
if got := deref(btn.AXLabel); got != "Allow While Using App" {
t.Errorf("accept button = %q, want %q (highest-preference present)", got, "Allow While Using App")
}
}
func TestFindAlertButtonSingleButtonAlert(t *testing.T) {
roots, _ := parseDescribeUI([]byte(okAlert))
// A one-button alert taps its only button for either intent.
for _, intent := range []alertIntent{intentAccept, intentDismiss} {
btn, ok := findAlertButton(roots, intent)
if !ok || deref(btn.AXLabel) != "OK" {
t.Errorf("intent %v: got (%q,%v), want (OK,true)", intent, deref(btn.AXLabel), ok)
}
}
}
func TestFindAlertButtonNoAlert(t *testing.T) {
// sampleTree (from main_test.go) has a "Play all" button but no alert and
// no standard alert-button labels, so neither intent resolves.
roots, _ := parseDescribeUI([]byte(sampleTree))
if _, ok := findAlertButton(roots, intentAccept); ok {
t.Error("expected no accept button on a normal screen")
}
if _, ok := findAlertButton(roots, intentDismiss); ok {
t.Error("expected no dismiss button on a normal screen")
}
}
func TestParseIntent(t *testing.T) {
if i, ok := parseIntent("accept"); !ok || i != intentAccept {
t.Errorf("accept => (%v,%v)", i, ok)
}
if i, ok := parseIntent("dismiss"); !ok || i != intentDismiss {
t.Errorf("dismiss => (%v,%v)", i, ok)
}
if _, ok := parseIntent("maybe"); ok {
t.Error("maybe should be rejected")
}
}
func TestTapArgsPrefersID(t *testing.T) {
roots, _ := parseDescribeUI([]byte(permissionAlert))
btn, _ := findAlertButton(roots, intentAccept) // "Allow" has AXUniqueId allow.btn
got := tapArgs(btn, "UDID1")
want := []string{"tap", "--tap-style", "physical", "--id", "allow.btn", "--udid", "UDID1"}
if !equalStrings(got, want) {
t.Errorf("tapArgs = %v, want %v", got, want)
}
}
func TestTapArgsFallsBackToLabel(t *testing.T) {
roots, _ := parseDescribeUI([]byte(permissionAlert))
btn, _ := findAlertButton(roots, intentDismiss) // "Don't Allow", no AXUniqueId
got := tapArgs(btn, "UDID1")
want := []string{"tap", "--tap-style", "physical", "--label", "Don't Allow", "--udid", "UDID1"}
if !equalStrings(got, want) {
t.Errorf("tapArgs = %v, want %v", got, want)
}
}
func equalStrings(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}