// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package review import ( "context" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" ) func TestFlatReviewRetainsHealthyContentAndReportsOmissions(t *testing.T) { for _, issue := range []atspi.VisibleText{ {Limited: true, Issue: atspi.TextCharacterLimit}, {Issue: atspi.TextProviderFailure}, } { bad := flatNode("/bad", "", 0, 30) bad.Snapshot.Name = "Unavailable object name must not replace its text" bad.Text = issue good := flatNode("/good", "Readable content", 0, 0) cursor := &FlatCursor{} if err := cursor.Refresh(context.Background(), flatTree(t, good, bad), objectID("/good")); err != nil { t.Fatal(err) } position, _, err := cursor.Read(Line, 0) if err != nil || position.Text != "Readable content" || cursor.completeness.OmittedObjects != 1 { t.Fatalf("partial review = %+v, %+v, %v", position, cursor.completeness, err) } if _, moved, err := cursor.Read(Line, 1); err != nil || moved { t.Fatalf("omitted object's name became content: moved=%v err=%v", moved, err) } } } func TestFlatReviewReportsUnavailableChildrenWithoutDroppingParentText(t *testing.T) { parent := flatNode("/parent", "Parent text", 0, 0) parent.ChildrenUnavailable = true cursor := &FlatCursor{} if err := cursor.Refresh(context.Background(), flatTree(t, parent), parent.Snapshot.ID()); err != nil { t.Fatal(err) } position, _, err := cursor.Read(Line, 0) if err != nil || position.Text != "Parent text" || cursor.completeness.UnavailableSubtrees != 1 { t.Fatalf("partial subtree = %+v, %+v, %v", position, cursor.completeness, err) } } func TestFlatReviewReportsUnavailableChildrenWithoutComponentBounds(t *testing.T) { parent := node("/parent", "") parent.Geometry = atspi.Geometry{VisibilityKnown: true, Visible: true, Showing: true} parent.ChildrenUnavailable = true good := flatNode("/good", "Readable", 0, 0) cursor := &FlatCursor{} if err := cursor.Refresh(context.Background(), flatTree(t, parent, good), good.Snapshot.ID()); err != nil { t.Fatal(err) } if cursor.completeness.UnavailableSubtrees != 1 { t.Fatalf("missing component bounds hid a failed subtree: %+v", cursor.completeness) } } func TestFlatReviewAnchorsPrunedFocusAtDisplayedParent(t *testing.T) { before := flatNode("/before", "Unrelated first line", 0, 0) parent := flatNode("/parent", "Focused parent text", 0, 30) leaf := node("/leaf", "") parent.Children = []atspi.ObjectID{leaf.Snapshot.ID()} root := flatNode("/window", "", 0, 0) root.Text = atspi.VisibleText{} root.Geometry.Bounds.Height = 100 root.Snapshot.Role = "frame" root.Children = []atspi.ObjectID{before.Snapshot.ID(), parent.Snapshot.ID()} tree, err := atspi.NewAccessibleTree(root.Snapshot.ID(), []atspi.TreeNode{root, before, parent, leaf}) if err != nil { t.Fatal(err) } cursor := &FlatCursor{} if err := cursor.Refresh(context.Background(), tree, leaf.Snapshot.ID()); err != nil { t.Fatal(err) } position, _, err := cursor.Read(Line, 0) if err != nil || position.Object != parent.Snapshot.ID() || position.Text != "Focused parent text" { t.Fatalf("pruned focus anchor = %+v, %v", position, err) } } func TestFlatWorkerWarnsOnceAndClearsRecoveredCompleteness(t *testing.T) { capturer := &fakeCapturer{calls: make(chan captureCall, 1)} worker, cancel := startWorker(t, capturer, time.Second) defer cancel() defer worker.Stop(context.Background()) good := flatNode("/good", "Readable", 0, 0) bad := flatNode("/bad", "Unavailable", 0, 30) bad.Text = atspi.VisibleText{Limited: true} tree := flatTree(t, good, bad) worker.SyncFocus(good.Snapshot.ID()) worker.ReadFlat(Line, 0) call := <-capturer.calls call.result <- captureResult{tree: tree} for index := 0; index < 2; index++ { if index != 0 { worker.ReadFlat(Character, 0) } result := waitResult(t, worker) if result.Err != nil || !result.FlatCompleteness.Incomplete() || result.FlatWarning != (index == 0) { t.Fatalf("partial result %d: %+v", index, result) } } bad = flatNode("/bad", "Recovered", 0, 30) fresh := flatTree(t, good, bad) snapshot, err := fresh.ReviewWindow(context.Background(), good.Snapshot.ID()) if err != nil { t.Fatal(err) } if err := tree.Replace(objectID("/window"), snapshot.Nodes); err != nil { t.Fatal(err) } worker.ReadFlat(Line, 1) result := waitResult(t, worker) if result.Err != nil || result.FlatCompleteness.Incomplete() || result.FlatWarning || result.FlatPosition.Text != "Recovered" { t.Fatalf("recovery result = %+v", result) } } func TestFlatReviewCompositeControlKeepsLabelBesideChildAction(t *testing.T) { for _, role := range []string{"page tab", "menu item"} { for _, withLabel := range []bool{false, true} { t.Run(role+map[bool]string{false: " action only", true: " with text label"}[withLabel], func(t *testing.T) { parent := flatNode("/parent", "", 0, 0) parent.Snapshot.Role, parent.Snapshot.Name = role, "Primary label" parent.Text = atspi.VisibleText{Known: true} child := flatNode("/child", "", 150, 0) child.Geometry.Bounds.Width = 20 child.Snapshot.Role, child.Snapshot.Name = "button", "Close" child.Text = atspi.VisibleText{} parent.Children = []atspi.ObjectID{child.Snapshot.ID()} nodes := []atspi.TreeNode{parent, child} if withLabel { label := flatNode("/label", "Primary label", 0, 0) label.Snapshot.Role = "static" label.Geometry.Bounds.Width = 100 label.Text.Ranges[0].Bounds = label.Geometry.Bounds parent.Children = append(parent.Children, label.Snapshot.ID()) nodes[0] = parent nodes = append(nodes, label) } lines, status, err := build_flat_layout(context.Background(), nodes) if err != nil || status.Incomplete() { t.Fatalf("layout: %+v %v", status, err) } names, actions := 0, 0 for _, line := range lines { for _, zone := range line.zones { if zone.text == "Primary label" { names++ } if zone.text == "Close" { actions++ } } } if names != 1 || actions != 1 { t.Fatalf("label count=%d action count=%d", names, actions) } }) } } }