diff --git a/internal/app/browser_actions_native_test.go b/internal/app/browser_actions_native_test.go index b2ef482..6148c80 100644 --- a/internal/app/browser_actions_native_test.go +++ b/internal/app/browser_actions_native_test.go @@ -147,34 +147,56 @@ func TestNativeBrowserActionOwnership(t *testing.T) { t.Fatalf("clickable discovery for %s: %v", name, found) } } + worker, err := review.NewWorker(adapter, 2*time.Second) + if err != nil { + t.Fatal(err) + } + if err := worker.Start(ctx); err != nil { + t.Fatal(err) + } + defer worker.Stop(context.Background()) + worker.SyncFocus(focus) + await_result := func() review.Result { + t.Helper() + select { + case result := <-worker.Results(): + if result.Err != nil || result.CaretErr != nil { + t.Fatalf("document command failed: %+v", result) + } + return result + case <-ctx.Done(): + t.Fatal(ctx.Err()) + return review.Result{} + } + } + move_caret := func(command review.DocumentCaretCommand) review.Result { + t.Helper() + if err := worker.MoveDocumentCaret(command); err != nil { + t.Fatal(err) + } + return await_result() + } + previousContent := "Initial mode content" for _, mode := range []string{"Basic", "Advanced"} { - // Reacquire references after each iframe replacement. Enter's target - // still comes from the text leaf, with native focus on the start heading. - tree, err = adapter.CaptureDocument(ctx, focus) - if err != nil { - t.Fatal(err) - } - root, _ = tree.Root() - document, err = tree.Document(ctx, root) - if err != nil { - t.Fatal(err) - } - var label atspi.ObjectID - for _, node := range document.Nodes { - if node.Snapshot.Role == "static" && node.Snapshot.Name == strings.ToUpper(mode) { - label = node.Reference.ID() + // Use the reader's command path so asynchronous replacement events + // exercise its bounded recapture and cursor recovery. + move_caret(review.CaretDocumentStart) + reached := false + for step := 0; step < 16; step++ { + move := move_caret(review.CaretNextLine) + if strings.TrimSpace(move.DocumentCaretText) == strings.ToUpper(mode) { + reached = true + break } } - cursor, _ = review.NewDocumentCursor(tree) - if _, err := cursor.SyncFocus(ctx, label); err != nil { + if !reached { + t.Fatalf("browse caret could not reach %s", mode) + } + if err := worker.ActivateDocumentCaret(); err != nil { t.Fatal(err) } - target, err := cursor.ActivationTarget() - if err != nil { - t.Fatal(err) - } - if err := adapter.ActivateDocumentObject(ctx, target, focus); err != nil { - t.Fatal(err) + if result := await_result(); !result.DocumentActivate { + t.Fatalf("missing activation result: %+v", result) } activated := false for !activated { @@ -197,11 +219,15 @@ func TestNativeBrowserActionOwnership(t *testing.T) { if err != nil { t.Fatal(err) } - ready := false + ready, obsolete := false, false for _, node := range document.Nodes { ready = ready || node.Snapshot.Name == mode+" mode content" + obsolete = obsolete || node.Snapshot.Name == previousContent } if ready { + if obsolete { + t.Fatalf("replacement retained %q", previousContent) + } break } select { @@ -212,11 +238,8 @@ func TestNativeBrowserActionOwnership(t *testing.T) { } readNewContent := false for step := 0; step < 8; step++ { - move, err := cursor.MoveCaret(ctx, adapter, review.CaretNextLine) - if err != nil { - t.Fatalf("reading after %s activation: %v", mode, err) - } - if strings.TrimSpace(move.Text) == mode+" mode content" { + move := move_caret(review.CaretNextLine) + if strings.TrimSpace(move.DocumentCaretText) == mode+" mode content" { readNewContent = true break } @@ -224,5 +247,6 @@ func TestNativeBrowserActionOwnership(t *testing.T) { if !readNewContent { t.Fatalf("browse caret could not reach %s replacement content", mode) } + previousContent = mode + " mode content" } } diff --git a/internal/app/browser_link_sounds_native_test.go b/internal/app/browser_link_sounds_native_test.go index ff96b18..f3bce45 100644 --- a/internal/app/browser_link_sounds_native_test.go +++ b/internal/app/browser_link_sounds_native_test.go @@ -140,6 +140,16 @@ func TestNativeBrowserCaretLinkSounds(t *testing.T) { if !heading { t.Fatal("fixture heading focus missing") } + // Focus speech can arrive before asynchronous mode resolution installs + // the browse grabs. Wait for X11 to acknowledge them before sending the + // first caret command; native Ctrl+Home at the heading can emit no event. + for !commands.BrowseNavigationEnabled() { + select { + case <-time.After(5 * time.Millisecond): + case <-ctx.Done(): + t.Fatal("browse grabs were not installed", ctx.Err()) + } + } check := func(chord, want string, pan *float64) float64 { t.Helper() key(chord)