// 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 TestDocumentListPreservesCursorAndSelectsExactTarget(t *testing.T) { tree, cursor := webCursor(t) ctx := context.Background() if _, err := cursor.SyncFocus(ctx, webID("/doc")); err != nil { t.Fatal(err) } capturer := &readAllCapturer{tree: tree} worker, _ := NewWorker(capturer, time.Second) worker.SyncFocus(webID("/doc")) before := cursor.diagnostic_state() list, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading}) if err != nil || len(list.Entries) != 2 { t.Fatalf("heading list: %+v %v", list, err) } if after := cursor.diagnostic_state(); after != before { t.Fatalf("opening moved cursor: %+v -> %+v", before, after) } result, cursor := worker.selectDocumentList(ctx, cursor, &documentListSelection{list: list, index: 1}) if result.Err != nil || !result.Moved || result.DocumentPosition.Object != list.Entries[1].Object { t.Fatalf("selection: %+v", result) } list, err = worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading}) if err != nil || list.Selected != 1 { t.Fatalf("did not preserve current row: %+v %v", list, err) } filtered, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading, level: 2}) if err != nil || len(filtered.Entries) != 2 || filtered.Entries[0].Snapshot.HeadingLevel != 2 { t.Fatalf("level filter: %+v %v", filtered, err) } } func TestDocumentListRejectsChangedTargetsAndOwnership(t *testing.T) { for _, change := range []string{"target removed", "target reused", "target hidden", "focus changed", "document reloaded"} { t.Run(change, func(t *testing.T) { tree, cursor := webCursor(t) ctx := context.Background() worker, _ := NewWorker(&readAllCapturer{tree: tree}, time.Second) worker.SyncFocus(webID("/doc")) list, err := worker.listDocument(ctx, cursor, &documentListRequest{ctx: ctx, target: DocumentHeading}) if err != nil { t.Fatal(err) } switch change { case "target removed": tree.Invalidate(list.Entries[0].Object.ID()) case "target reused": tree.Invalidate(list.Entries[0].Object.ID()) if err := tree.Replace(webID("/root"), webNodes()); err != nil { t.Fatal(err) } case "target hidden": nodes := webNodes() nodes[3].Geometry = atspi.Geometry{VisibilityKnown: true} if err := tree.Replace(webID("/root"), nodes); err != nil { t.Fatal(err) } case "focus changed": worker.SyncFocus(webID("/chrome")) case "document reloaded": worker.ResetDocument(webID("/doc")) } result, _ := worker.selectDocumentList(ctx, cursor, &documentListSelection{list: list, index: 0}) if result.Err == nil || result.Moved { t.Fatalf("stale selection succeeded: %+v", result) } }) } } func TestDocumentListDescendantLabelsExcludeHiddenAndPasswordText(t *testing.T) { node := func(path, role, text string, children ...string) atspi.TreeNode { snapshot := atspi.Snapshot{BusName: webID(path).BusName, Path: path, Role: role, Text: text} entry := atspi.TreeNode{Snapshot: snapshot} for _, child := range children { entry.Children = append(entry.Children, webID(child)) } return entry } nodes := []atspi.TreeNode{ node("/doc", "document web", "", "/clickable"), node("/clickable", "section", "", "/text", "/hidden", "/password"), node("/text", "static", "Clickable text"), node("/hidden", "static", "Hidden content"), node("/password", "password text", "private fixture password"), } nodes[1].Snapshot.SupportsClick = true nodes[3].Geometry = atspi.Geometry{VisibilityKnown: true} tree, err := atspi.NewAccessibleTree(webID("/doc"), nodes) if err != nil { t.Fatal(err) } worker, _ := NewWorker(&readAllCapturer{tree: tree}, time.Second) worker.SyncFocus(webID("/doc")) ctx := context.Background() list, err := worker.listDocument(ctx, nil, &documentListRequest{ctx: ctx, target: DocumentClickable}) if err != nil || len(list.Entries) != 1 || list.Entries[0].Label != "Clickable text" { t.Fatalf("unsafe or missing descendant label: %+v %v", list, err) } } func TestWorkerDocumentListUsesOrderedRequests(t *testing.T) { tree, _ := webCursor(t) worker, cancel := startWorker(t, &readAllCapturer{tree: tree}, time.Second) defer cancel() worker.SyncFocus(webID("/doc")) ctx, stop := context.WithTimeout(context.Background(), time.Second) defer stop() list, err := worker.ListDocument(ctx, DocumentHeading, 0) if err != nil { t.Fatal(err) } if err := worker.SelectDocumentList(list, 1); err != nil { t.Fatal(err) } result := waitResult(t, worker) if result.Err != nil || !result.Moved || result.DocumentPosition.Object != list.Entries[1].Object { t.Fatalf("queued selection: %+v", result) } } func TestAdditionalDocumentListRoles(t *testing.T) { for target, roles := range map[DocumentTarget][]string{DocumentAnnotation: {"content deletion", "content insertion", "mark"}, DocumentInternalFrame: {"internal frame"}, DocumentMath: {"math"}} { for _, role := range roles { if !documentTargetMatches(target, atspi.DocumentNode{Snapshot: atspi.Snapshot{Role: role}}, 0) { t.Fatalf("missing %s", role) } } } }