// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "context" "errors" "strings" "testing" ) type longReviewText struct { runes []rune queries int largest int } func (source *longReviewText) count(context.Context) (int, error) { return len(source.runes), nil } func (source *longReviewText) line(context.Context, int) (TextRange, error) { return TextRange{Start: 0, End: len(source.runes), Text: string(source.runes)}, nil } func (source *longReviewText) bounds(ctx context.Context, start, end int) (Rectangle, error) { return bounded_review_bounds(ctx, start, end, func(_ context.Context, first, last int) (Rectangle, error) { source.queries++ source.largest = max(source.largest, last-first) if last-first > 128 { return Rectangle{}, errors.New("provider timed out measuring a long range") } return Rectangle{X: int32(first * 10), Width: int32((last - first) * 10), Height: 20}, nil }) } func TestCaptureVisibleTextBoundsLongHorizontalLine(t *testing.T) { source := &longReviewText{runes: []rune(strings.Repeat("é猫", 1712))} clip := Rectangle{X: 1000, Width: 300, Height: 20} got, err := captureVisibleText(context.Background(), source, clip) if err != nil || !got.Known || got.Limited { t.Fatalf("long line: %+v, %v", got, err) } var spoken strings.Builder for _, part := range got.Ranges { if part.Start < 100 || part.End > 130 { t.Fatalf("offscreen range retained: %+v", part) } spoken.WriteString(part.Text) } if spoken.String() != string(source.runes[100:130]) || source.largest > 128 || source.queries > 400 { t.Fatalf("clipping: text_bytes=%d largest_query=%d queries=%d", spoken.Len(), source.largest, source.queries) } if !got.valid(Geometry{BoundsKnown: true, Bounds: clip}) { t.Fatal("long clipped line violates owned text invariants") } } func TestBoundedReviewBoundsIncludesInteriorAndEmptyChunks(t *testing.T) { rectangles := []Rectangle{{X: 500, Width: 100, Height: 20}, {X: -50, Width: 300, Height: 20}, {}, {X: 400, Width: 50, Height: 20}} queries := 0 got, err := bounded_review_bounds(context.Background(), 0, 512, func(_ context.Context, start, end int) (Rectangle, error) { if end-start > 128 { t.Fatalf("oversized query: %d", end-start) } queries++ return rectangles[start/128], nil }) if err != nil || got != (Rectangle{X: -50, Width: 650, Height: 20}) || queries != 4 { t.Fatalf("union lost interior bidi or empty chunks: %+v, %v, queries=%d", got, err, queries) } } func TestBoundedReviewBoundsStopsOnFailureOrCancellation(t *testing.T) { providerError := errors.New("provider unavailable") if _, err := bounded_review_bounds(context.Background(), 0, 512, func(context.Context, int, int) (Rectangle, error) { return Rectangle{}, providerError }); !errors.Is(err, providerError) { t.Fatalf("provider error hidden: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() queries := 0 _, err := bounded_review_bounds(ctx, 0, 512, func(context.Context, int, int) (Rectangle, error) { queries++ cancel() return Rectangle{Width: 100, Height: 20}, nil }) if !errors.Is(err, context.Canceled) || queries != 1 { t.Fatalf("cancellation: %v queries=%d", err, queries) } } // Even a block entirely inside the viewport can contain zero-area glyphs. // A horizontally clipped line must retain its existing per-glyph exclusions. type zeroGlyphReviewText struct{ longReviewText } func (source *zeroGlyphReviewText) bounds(ctx context.Context, start, end int) (Rectangle, error) { if start == 5 && end == 6 { return Rectangle{}, ctx.Err() } return source.longReviewText.bounds(ctx, start, end) } func TestCaptureVisibleTextClippedInteriorBlockExcludesZeroAreaGlyph(t *testing.T) { source := &zeroGlyphReviewText{longReviewText: longReviewText{runes: []rune(strings.Repeat("x", 400))}} got, err := captureVisibleText(context.Background(), source, Rectangle{Width: 2000, Height: 20}) if err != nil || !got.Known || got.Limited { t.Fatalf("capture: %+v, %v", got, err) } characters := 0 for _, span := range got.Ranges { if span.Start <= 5 && span.End > 5 { t.Fatalf("zero-area glyph included: %+v", span) } characters += span.End - span.Start } if characters != 199 { t.Fatalf("visible characters=%d, want 199", characters) } }