test: harden the e2e harness and unify fixture AX labels

The harness now fails setup loudly when the fixture build or AX trust is
missing, rebuilds the fixture when sources are newer than the bundle,
asserts hover only against a freshly observed state, and force-collapses
the disclosure so the expand test proves a real flip. The slider/stepper
labels live solely on the NSViews (the AX-actionable elements), removing
the macOS-version-dependent race between two label sources, and the drag
canvas reports a zero frame when detached from a window.
This commit is contained in:
Lahfir 2026-06-10 16:17:54 -07:00
parent afad271112
commit c73beb7f6f
3 changed files with 42 additions and 13 deletions

View file

@ -124,7 +124,25 @@ trap cleanup EXIT
# --- Setup -----------------------------------------------------------------
note "Setup"
[ -x "$bin" ] || { echo "release binary missing; run 'cargo build --release'"; exit 2; }
[ -d "$fixture_app" ] || "$repo/tests/fixture-app/build.sh" >/dev/null
ax_state="$("$bin" permissions 2>/dev/null | field "['data']['accessibility']['state']")"
if [ "$ax_state" != "granted" ]; then
echo "accessibility permission not granted (state='${ax_state:-unknown}')." >&2
echo "Grant Accessibility trust to this terminal/runner in System Settings > Privacy & Security, then retry." >&2
exit 2
fi
# Rebuild when the bundle is missing OR any fixture source is newer than it, so
# the suite never silently exercises a stale fixture. Keep swiftc stderr
# visible: a failed build must abort loudly, not cascade into launch noise.
fixture_stale() {
[ ! -d "$fixture_app" ] ||
[ -n "$(find "$repo/tests/fixture-app" -maxdepth 1 -name '*.swift' -newer "$fixture_app" 2>/dev/null)" ]
}
if fixture_stale; then
"$repo/tests/fixture-app/build.sh" >/dev/null || {
echo "fixture build failed (swiftc output above); cannot run E2E" >&2
exit 2
}
fi
"$bin" close-app "$app" --force >/dev/null 2>&1 || true; sleep 1
open "$fixture_app"
ready=""; tries=0
@ -202,10 +220,14 @@ def f(n):
if r: return r
print(f(d['data']['tree']) or '')" 2>/dev/null)"
hv_b="$(read_value hover-status)"
[ -n "$hv_xy" ] && "$bin" --headed hover --xy "$hv_xy" >/dev/null 2>&1; sleep 0.5
hv_a="$(read_value hover-status)"
assert "--headed hover triggers onHover" "$([ "$hv_a" = "hovered" ] && echo 1 || echo 0)" \
"xy='$hv_xy' hover-status before='$hv_b' after='$hv_a'"
if [ -n "$hv_xy" ]; then
"$bin" --headed hover --xy "$hv_xy" >/dev/null 2>&1; sleep 0.5
hv_a="$(read_value hover-status)"
assert "--headed hover triggers onHover" "$([ "$hv_a" = "hovered" ] && echo 1 || echo 0)" \
"xy='$hv_xy' hover-status before='$hv_b' after='$hv_a'"
else
assert "--headed hover triggers onHover" 0 "hover-target bounds not found in snapshot (no xy to hover)"
fi
# --- Strict resolution -----------------------------------------------------
# Two buttons share role+name "twin-control" and each records a distinct effect
@ -364,16 +386,21 @@ dr_a="$(read_value drag-canvas-status)"
assert "drag delivered a gesture" "$(echo "$dr_a" | grep -q '^dragged-' && echo 1 || echo 0)" "from='$from_xy' to='$to_xy' canvas before='$dr_b' after='$dr_a'"
note "expand a press-toggled disclosure (verified by disclosure value)"
# Force-collapse first so the expand below proves a real state flip — an
# already-expanded disclosure would make "expand succeeded" vacuous.
"$bin" collapse "$(resolve disclosure disclosure-section)" >/dev/null 2>&1; sleep 0.4
exp_b="$("$bin" get "$(resolve disclosure disclosure-section)" 2>/dev/null | field "['data']['value']")"
eout="$("$bin" expand "$(resolve disclosure disclosure-section)" 2>&1)"; sleep 0.4
exp_a="$("$bin" get "$(resolve disclosure disclosure-section)" 2>/dev/null | field "['data']['value']")"
eok="$(echo "$eout" | field "['ok']")"
if [ "$exp_a" = "true" ]; then
assert "expand set disclosure expanded" 1 "value before='$exp_b' after='$exp_a' cmd_ok=$eok"
if [ "$exp_b" = "true" ]; then
skip "expand unprovable: pre-collapse did not land, disclosure already expanded [value before='$exp_b' after='$exp_a' cmd_ok=$eok]"
elif [ "$exp_a" = "true" ]; then
assert "expand flipped disclosure from collapsed to expanded" 1 "value before='$exp_b' after='$exp_a' cmd_ok=$eok"
elif [ "$eok" = "False" ]; then
skip "expand honestly failed (disclosure not AX-actionable) [value before='$exp_b' after='$exp_a' cmd_ok=$eok]"
else
assert "expand set disclosure expanded" 0 "claimed success but value before='$exp_b' after='$exp_a' cmd_ok=$eok"
assert "expand flipped disclosure from collapsed to expanded" 0 "claimed success but value before='$exp_b' after='$exp_a' cmd_ok=$eok"
fi
# --- Performance (CLI wall-clock per operation) ----------------------------

View file

@ -166,11 +166,12 @@ struct ContentView: View {
// Native AppKit slider and stepper: unlike SwiftUI's, these expose a
// working AX value/increment interface, so set-value can drive them.
// The harness-facing labels (value-slider/value-stepper) live on the
// NSViews themselves the AX-actionable elements so exactly one
// label source exists regardless of how SwiftUI wraps them.
NativeSlider(value: $nativeSliderValue).frame(width: 180, height: 20)
.accessibilityLabel("value-slider")
StatusReadout(name: "slider-status", value: String(Int(nativeSliderValue)))
NativeStepper(value: $nativeStepperValue)
.accessibilityLabel("value-stepper")
StatusReadout(name: "stepper-status", value: String(Int(nativeStepperValue)))
DisclosureGroup("Disclosure Section", isExpanded: $disclosureExpanded) {

View file

@ -34,7 +34,7 @@ struct NativeSlider: NSViewRepresentable {
let s = NSSlider(
value: value, minValue: 0, maxValue: 100,
target: context.coordinator, action: #selector(Coordinator.changed(_:)))
s.setAccessibilityLabel("native-slider")
s.setAccessibilityLabel("value-slider")
return s
}
func updateNSView(_ view: NSSlider, context: Context) { view.doubleValue = value }
@ -51,7 +51,7 @@ struct NativeStepper: NSViewRepresentable {
s.valueWraps = false
s.target = context.coordinator
s.action = #selector(NativeControlCoordinator.changed(_:))
s.setAccessibilityLabel("native-stepper")
s.setAccessibilityLabel("value-stepper")
return s
}
func updateNSView(_ view: NSStepper, context: Context) { view.doubleValue = value }
@ -92,7 +92,8 @@ final class DragCanvasView: NSView {
setAccessibilityLabel("drag-canvas")
}
override func accessibilityFrame() -> NSRect {
window?.convertToScreen(convert(bounds, to: nil)) ?? bounds
guard let window else { return .zero }
return window.convertToScreen(convert(bounds, to: nil))
}
override func isAccessibilityElement() -> Bool { true }
override func mouseDown(with e: NSEvent) { start = convert(e.locationInWindow, from: nil); dragged = false }