120 lines
4.3 KiB
Go
120 lines
4.3 KiB
Go
// 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)
|
|
}
|
|
}
|