diff --git a/internal/driver/ioscompanion/driver_test.go b/internal/driver/ioscompanion/driver_test.go
index 05bd19e..f163bf5 100644
--- a/internal/driver/ioscompanion/driver_test.go
+++ b/internal/driver/ioscompanion/driver_test.go
@@ -4,11 +4,14 @@ import (
"bytes"
"context"
"errors"
+ "fmt"
"image"
"image/color"
"image/png"
"net"
+ "os"
"os/exec"
+ "path/filepath"
"strings"
"testing"
"time"
@@ -573,3 +576,154 @@ func TestIsConnectionErrorRecognizesUnavailableSentinel(t *testing.T) {
t.Fatal("ordinary errors must not count as connection errors")
}
}
+
+// fakeRunnerCompanion stands in for the in-simulator runner half of the hybrid:
+// it serves snapshots and native typing.
+type fakeRunnerCompanion struct {
+ fakeCompanion
+
+ typed []struct {
+ text string
+ replace bool
+ }
+}
+
+func (f *fakeRunnerCompanion) TypeText(_ context.Context, text string, replace bool) error {
+ f.record("typetext")
+ f.typed = append(f.typed, struct {
+ text string
+ replace bool
+ }{text, replace})
+ return nil
+}
+
+var _ transport.TextTyper = (*fakeRunnerCompanion)(nil)
+
+func newHybridTestDriver(legacy *fakeCompanion, runner *fakeRunnerCompanion) *Driver {
+ d := newTestDriver(legacy)
+ d.hybrid = true
+ d.runnerClient = runner
+ return d
+}
+
+func TestHybridInputTextClearsViaHIDThenTypes(t *testing.T) {
+ legacy := &fakeCompanion{accessibilityJSON: "[]"}
+ runner := &fakeRunnerCompanion{}
+ d := newHybridTestDriver(legacy, runner)
+
+ if err := d.InputText(context.Background(), "héllo 🌟"); err != nil {
+ t.Fatal(err)
+ }
+ if len(legacy.calls) != 1 || legacy.calls[0] != "hid" {
+ t.Fatalf("legacy calls = %v, want exactly the clear chord", legacy.calls)
+ }
+ if len(runner.typed) != 1 || runner.typed[0].text != "héllo 🌟" || runner.typed[0].replace {
+ t.Fatalf("typed = %+v, want the text appended without replace", runner.typed)
+ }
+}
+
+func TestHybridSnapshotsComeFromRunner(t *testing.T) {
+ legacy := &fakeCompanion{accessibilityJSON: `[{"type":"Application","frame":{"x":0,"y":0,"width":1,"height":1},"enabled":true}]`}
+ runner := &fakeRunnerCompanion{}
+ runner.accessibilityJSON = `[{"type":"Button","AXUniqueId":"FromRunner","frame":{"x":0,"y":0,"width":10,"height":10},"enabled":true}]`
+ d := newHybridTestDriver(legacy, runner)
+
+ hierarchy, err := d.Hierarchy(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(hierarchy, "FromRunner") {
+ t.Fatalf("hierarchy = %s, want runner content", hierarchy)
+ }
+ for _, call := range legacy.calls {
+ if call == "accessibility" {
+ t.Fatal("hybrid must not read snapshots from the legacy companion")
+ }
+ }
+}
+
+func TestHybridLaunchSkipsPasteGrant(t *testing.T) {
+ legacy := &fakeCompanion{accessibilityJSON: "[]"}
+ runner := &fakeRunnerCompanion{}
+ d := newHybridTestDriver(legacy, runner)
+ granted := 0
+ d.grantPaste = func(context.Context) error { granted++; return nil }
+
+ if err := d.Launch(context.Background(), "", false, nil); err != nil {
+ t.Fatal(err)
+ }
+ if granted != 0 {
+ t.Fatalf("grantPaste called %d times, want 0 on the hybrid path", granted)
+ }
+}
+
+func TestHybridGesturesStayOnLegacyHID(t *testing.T) {
+ legacy := &fakeCompanion{accessibilityJSON: "[]"}
+ runner := &fakeRunnerCompanion{}
+ d := newHybridTestDriver(legacy, runner)
+
+ if err := d.DoubleTap(context.Background(), 10, 20); err != nil {
+ t.Fatal(err)
+ }
+ if len(legacy.calls) != 1 || legacy.calls[0] != "hid" {
+ t.Fatalf("legacy calls = %v, want the double-tap HID stream", legacy.calls)
+ }
+ for _, call := range runner.calls {
+ if call == "hid" {
+ t.Fatal("runner must not receive gesture streams")
+ }
+ }
+}
+
+func TestHybridRunnerErrorTriggersRestart(t *testing.T) {
+ legacy := &fakeCompanion{accessibilityJSON: "[]"}
+ runner := &fakeRunnerCompanion{}
+ runner.accessibilityErr = fmt.Errorf("read snapshot: %w", transport.ErrCompanionUnavailable)
+ d := newHybridTestDriver(legacy, runner)
+ restarts := 0
+ d.restart = func(context.Context) error {
+ restarts++
+ runner.accessibilityErr = nil
+ return nil
+ }
+
+ if _, err := d.Hierarchy(context.Background()); err != nil {
+ t.Fatal(err)
+ }
+ if restarts != 1 {
+ t.Fatalf("restarts = %d, want 1", restarts)
+ }
+}
+
+func TestBindTestRunPortSubstitutesPlaceholder(t *testing.T) {
+ directory := t.TempDir()
+ source := filepath.Join(directory, "runner.xctestrun")
+ if err := os.WriteFile(source, []byte("__COMPANION_PORT__"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ bound, err := bindTestRunPort(source, "27999")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if filepath.Dir(bound) != directory {
+ t.Fatalf("bound copy %s must sit next to the original", bound)
+ }
+ content, err := os.ReadFile(bound)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if string(content) != "27999" {
+ t.Fatalf("bound content = %s", content)
+ }
+}
+
+func TestBindTestRunPortRejectsMissingPlaceholder(t *testing.T) {
+ directory := t.TempDir()
+ source := filepath.Join(directory, "runner.xctestrun")
+ if err := os.WriteFile(source, []byte("fixed"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := bindTestRunPort(source, "27999"); err == nil {
+ t.Fatal("expected an error for a configuration without the placeholder")
+ }
+}