diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index be38ccc..e48b4ed 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -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) ---------------------------- diff --git a/tests/fixture-app/AgentDeskFixture.swift b/tests/fixture-app/AgentDeskFixture.swift index 1d523f9..a1d3b3c 100644 --- a/tests/fixture-app/AgentDeskFixture.swift +++ b/tests/fixture-app/AgentDeskFixture.swift @@ -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) { diff --git a/tests/fixture-app/FixtureComponents.swift b/tests/fixture-app/FixtureComponents.swift index eaa19f6..7133c6a 100644 --- a/tests/fixture-app/FixtureComponents.swift +++ b/tests/fixture-app/FixtureComponents.swift @@ -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 }