// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "context" "errors" "reflect" "strings" "testing" ) type lineGeometryText struct { lineCalls int } func (*lineGeometryText) count(context.Context) (int, error) { return 1000000, nil } func (source *lineGeometryText) line(ctx context.Context, offset int) (TextRange, error) { source.lineCalls++ start := offset / 100 * 100 return TextRange{Start: start, End: start + 100, Text: strings.Repeat("é", 100)}, ctx.Err() } func (*lineGeometryText) bounds(ctx context.Context, start, end int) (Rectangle, error) { if end-start == 1 { return Rectangle{}, errors.New("character rectangles unavailable") } return Rectangle{Y: int32(start/100-4200) * 10, Width: 100, Height: 10}, ctx.Err() } func TestCaptureVisibleTextDiscoversLargeViewportFromLines(t *testing.T) { source := &lineGeometryText{} clip := Rectangle{Width: 100, Height: 20} got, err := captureVisibleText(context.Background(), source, clip) want := VisibleText{Known: true, Ranges: []TextRange{ {420000, 420100, strings.Repeat("é", 100), Rectangle{Width: 100, Height: 10}}, {420100, 420200, strings.Repeat("é", 100), Rectangle{Y: 10, Width: 100, Height: 10}}, }} if err != nil || !reflect.DeepEqual(got, want) || source.lineCalls > 40 { t.Fatalf("line viewport = %+v, %v; calls=%d", got, err, source.lineCalls) } } type spatialReviewText struct { clippedReviewText ranges []TextRange lineCalls, viewportCalls int cancel context.CancelFunc } func (source *spatialReviewText) line(ctx context.Context, offset int) (TextRange, error) { source.lineCalls++ return TextRange{}, errReviewLineTooLong } func (source *spatialReviewText) viewport(ctx context.Context, start, end int, clip Rectangle) ([]TextRange, error) { source.viewportCalls++ if source.cancel != nil { source.cancel() } return source.ranges, ctx.Err() } func TestCaptureVisibleTextRetrievesLongLineViewportWithoutWholeLine(t *testing.T) { source := &spatialReviewText{clippedReviewText: clippedReviewText{rows: 1, columns: 1000000}, ranges: []TextRange{ {Start: 500005, End: 500045, Text: strings.Repeat("é", 40)}, }} clip := Rectangle{X: 5000003, Width: 394, Height: 12} got, err := captureVisibleText(context.Background(), source, clip) want := VisibleText{Known: true, Ranges: []TextRange{{500005, 500045, strings.Repeat("é", 40), clip}}} if err != nil || !reflect.DeepEqual(got, want) || source.lineCalls != 0 || source.viewportCalls != 1 { t.Fatalf("spatial capture = %+v, %v; line=%d viewport=%d", got, err, source.lineCalls, source.viewportCalls) } } func TestCaptureVisibleTextRejectsInvalidSpatialOutput(t *testing.T) { for _, ranges := range [][]TextRange{ {{Start: 8, End: 10, Text: "x"}}, {{Start: 8, End: 10, Text: "é猫"}, {Start: 7, End: 8, Text: "x"}}, {{Start: 0, End: maxReviewCharacters + 1, Text: strings.Repeat("x", maxReviewCharacters+1)}}, {{Start: 999999, End: 1000001, Text: "xx"}}, } { source := &spatialReviewText{clippedReviewText: clippedReviewText{rows: 1, columns: 1000000}, ranges: ranges} got, err := captureVisibleText(context.Background(), source, Rectangle{Width: 100, Height: 12}) if err == nil || got.Known || len(got.Ranges) != 0 { t.Fatalf("invalid spatial capture accepted: %+v %v", got, err) } } } func TestCaptureVisibleTextSpatialCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() source := &spatialReviewText{clippedReviewText: clippedReviewText{rows: 1, columns: 1000000}, cancel: cancel} got, err := captureVisibleText(ctx, source, Rectangle{Width: 100, Height: 12}) if !errors.Is(err, context.Canceled) || got.Known || got.Limited { t.Fatalf("spatial cancellation = %+v %v", got, err) } } type pointReviewText struct { fakeReviewText pointCalls int textByOffset map[int]string } func (source *pointReviewText) offset_at_point(ctx context.Context, x, y int32) (int, error) { source.pointCalls++ for span, bounds := range source.rectangles { if span[1]-span[0] == 1 && int64(x) >= int64(bounds.X) && int64(x) < int64(bounds.X)+int64(bounds.Width) && int64(y) >= int64(bounds.Y) && int64(y) < int64(bounds.Y)+int64(bounds.Height) { return span[0], ctx.Err() } } return -1, ctx.Err() } func (source *pointReviewText) text_span(ctx context.Context, start, end int) (string, error) { var text strings.Builder for offset := start; offset < end; offset++ { character, ok := source.textByOffset[offset] if !ok { character = "x" } text.WriteString(character) } return text.String(), ctx.Err() } func TestPointReviewViewportKeepsDisjointBidiOffsets(t *testing.T) { source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {-100, 0, 10, 10}, {99999, 100000}: {100, 0, 10, 10}, {10, 11}: {30, 0, 10, 10}, {70000, 70001}: {10, 0, 10, 10}, {80000, 80001}: {20, 0, 10, 10}, }}, textByOffset: map[int]string{10: "א", 70000: "猫", 80000: "é"}} ranges, err := point_review_viewport(context.Background(), source, 0, 100000, Rectangle{X: 10, Width: 30, Height: 10}) if err != nil || len(ranges) != 3 || source.pointCalls != 3 { t.Fatalf("point bidi = %+v, %v; calls=%d", ranges, err, source.pointCalls) } for index, offset := range []int{10, 70000, 80000} { if ranges[index].Start != offset || ranges[index].Text != source.textByOffset[offset] { t.Fatalf("point bidi lost native offset: %+v", ranges) } } } func TestPointReviewViewportDoesNotSkipVerticallyInsetGlyph(t *testing.T) { source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {0, 0, 10, 20}, {1, 2}: {10, 5, 10, 10}, {2, 3}: {20, 0, 10, 20}, }}, textByOffset: map[int]string{0: "A", 1: "é", 2: "B"}} ranges, err := point_review_viewport(context.Background(), source, 0, 3, Rectangle{Width: 30, Height: 20}) if err != nil || len(ranges) != 3 || ranges[1].Text != "é" { t.Fatalf("inset glyph omitted: %+v %v", ranges, err) } } func TestPointReviewViewportBoundsSparseProviderWork(t *testing.T) { source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {-100, 0, 10, 100}, {99999, 100000}: {1000, 0, 10, 100}, }}} ranges, err := point_review_viewport(context.Background(), source, 0, 100000, Rectangle{Width: 100, Height: 100}) if !errors.Is(err, errReviewViewportScanLimit) || len(ranges) != 0 || source.pointCalls != 4096 { t.Fatalf("sparse provider work: ranges=%d calls=%d err=%v", len(ranges), source.pointCalls, err) } } func TestPointReviewViewportRetainsLineWithZeroAreaTrailingNewline(t *testing.T) { source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {Width: 10, Height: 10}, {1, 2}: {X: 10, Width: 10, Height: 10}, {2, 3}: {}, }}, textByOffset: map[int]string{0: "é", 1: "猫", 2: "\n"}} ranges, err := point_review_viewport(context.Background(), source, 0, 3, Rectangle{Width: 20, Height: 20}) if err != nil || len(ranges) != 1 || ranges[0].Text != "é猫" || ranges[0].End != 2 { t.Fatalf("trailing newline discarded visible glyphs: %+v %v", ranges, err) } } func TestCaptureVisibleTextSkipsZeroAreaGlyphsWithoutLosingOtherText(t *testing.T) { source := fakeReviewText{characters: 6, lines: map[int]TextRange{ 0: {Start: 0, End: 6, Text: "abcXYZ"}, }, rectangles: map[[2]int]Rectangle{ {0, 6}: {Width: 60, Height: 10}, {0, 1}: {Width: 10, Height: 10}, {1, 2}: {X: 10, Width: 10, Height: 10}, {2, 3}: {}, {3, 4}: {X: 20, Width: 10, Height: 10}, {4, 5}: {X: 40, Width: 10, Height: 10}, {5, 6}: {X: 50, Width: 10, Height: 10}, }} got, err := captureVisibleText(context.Background(), source, Rectangle{Width: 30, Height: 10}) want := VisibleText{Known: true, Ranges: []TextRange{ {0, 2, "ab", Rectangle{Width: 20, Height: 10}}, {3, 4, "X", Rectangle{X: 20, Width: 10, Height: 10}}, }} if err != nil || !reflect.DeepEqual(got, want) { t.Fatalf("zero-area glyph capture = %+v, %v; want %+v", got, err, want) } } func TestCaptureVisibleTextRetainsZeroAreaCombiningMark(t *testing.T) { source := fakeReviewText{characters: 4, lines: map[int]TextRange{ 0: {Start: 0, End: 4, Text: "a\u0301bc"}, }, rectangles: map[[2]int]Rectangle{ {0, 4}: {Width: 40, Height: 10}, {0, 1}: {Width: 10, Height: 10}, {1, 2}: {}, {2, 3}: {X: 10, Width: 10, Height: 10}, {3, 4}: {X: 30, Width: 10, Height: 10}, }} got, err := captureVisibleText(context.Background(), source, Rectangle{Width: 20, Height: 10}) if err != nil || !got.Known || len(got.Ranges) != 1 || got.Ranges[0].Text != "a\u0301b" || got.Ranges[0].End != 3 { t.Fatalf("visible accent lost: %+v %v", got, err) } } func TestPointReviewViewportRetainsZeroAreaCombiningMark(t *testing.T) { source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {Width: 10, Height: 10}, {1, 2}: {}, {2, 3}: {X: 10, Width: 10, Height: 10}, }}, textByOffset: map[int]string{0: "a", 1: "\u0301", 2: "b"}} ranges, err := point_review_viewport(context.Background(), source, 0, 3, Rectangle{Width: 20, Height: 10}) if err != nil || len(ranges) != 2 || ranges[0].Text != "a\u0301" || ranges[0].End != 2 || ranges[1].Text != "b" { t.Fatalf("point traversal lost accent: %+v %v", ranges, err) } } func TestPointReviewViewportRejectsExcessiveClusterWithoutPartialText(t *testing.T) { const finalOffset = maxReviewBoundsCharacters + 2 source := &pointReviewText{fakeReviewText: fakeReviewText{rectangles: map[[2]int]Rectangle{ {0, 1}: {Width: 10, Height: 10}, {finalOffset, finalOffset + 1}: {X: 10, Width: 10, Height: 10}, }}, textByOffset: map[int]string{0: "a", finalOffset: "b"}} for offset := 1; offset < finalOffset; offset++ { source.textByOffset[offset] = "\u0301" } ranges, err := point_review_viewport(context.Background(), source, 0, finalOffset+1, Rectangle{Width: 20, Height: 10}) if !errors.Is(err, errReviewViewportScanLimit) || len(ranges) != 0 { t.Fatalf("cluster limit silently omitted text: %+v %v", ranges, err) } }