229 lines
9.9 KiB
Go
229 lines
9.9 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"
|
|
"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)
|
|
}
|
|
}
|