126 lines
3.5 KiB
Go
126 lines
3.5 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/atspi"
|
|
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
|
)
|
|
|
|
func TestNativeBrowserTabs(t *testing.T) {
|
|
if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" && os.Getenv("CTHULHU_NATIVE_FIREFOX") != "1" {
|
|
t.Skip("requires a private browser session")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
|
defer cancel()
|
|
adapter := atspi.New()
|
|
if err := adapter.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer adapter.Stop(context.Background())
|
|
listener := xorg.New()
|
|
if err := listener.Start(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Stop(context.Background())
|
|
queue := &autocompleteRequestQueue{requests: make(chan speechRequest, 128)}
|
|
logFile, err := os.CreateTemp("/tmp", "cthulhu-tabs-events-*.log")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer logFile.Close()
|
|
t.Logf("event diagnostics: %s", logFile.Name())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
processEventsWithInput(ctx, slog.New(slog.NewTextHandler(logFile, &slog.HandlerOptions{Level: slog.LevelDebug})), queue, adapter, nil, nil, 0, nil, listener, nil, nil)
|
|
}()
|
|
defer func() { cancel(); <-done }()
|
|
keys := func(chord string) {
|
|
t.Helper()
|
|
if out, err := exec.CommandContext(ctx, "xdotool", "key", "--clearmodifiers", chord).CombinedOutput(); err != nil {
|
|
t.Fatalf("keys: %v %s", err, out)
|
|
}
|
|
}
|
|
wait := func(role, text string) speechRequest {
|
|
t.Helper()
|
|
for {
|
|
select {
|
|
case request := <-queue.requests:
|
|
t.Logf("speech: %s %q (%s, append=%v)", request.Role, request.Text, request.Cause, request.Append)
|
|
if request.Role == role && (text == "" || request.Text == text ||
|
|
role == "page tab" && strings.Contains(request.Text, text)) {
|
|
return request
|
|
}
|
|
case <-ctx.Done():
|
|
t.Fatalf("missing %s %q: %v", role, text, ctx.Err())
|
|
return speechRequest{}
|
|
}
|
|
}
|
|
}
|
|
settle := func(checkTab bool) {
|
|
t.Helper()
|
|
// Drain the asynchronous event burst before the next chord so a
|
|
// duplicate from one operation cannot satisfy the next assertion.
|
|
timer := time.NewTimer(150 * time.Millisecond)
|
|
defer timer.Stop()
|
|
for {
|
|
select {
|
|
case request := <-queue.requests:
|
|
if checkTab && request.Role == "page tab" {
|
|
t.Fatalf("extra tab announcement without another chord: %#v", request)
|
|
}
|
|
if checkTab && (request.Cause == atspi.TextDeletedEvent || request.Cause == atspi.TextInsertedEvent) {
|
|
t.Fatalf("automatic address rewrite spoken: %#v", request)
|
|
}
|
|
timer.Reset(150 * time.Millisecond)
|
|
case <-timer.C:
|
|
return
|
|
case <-ctx.Done():
|
|
t.Fatal(ctx.Err())
|
|
}
|
|
}
|
|
}
|
|
keys("ctrl+l")
|
|
// Firefox's address control is exposed as a combo box.
|
|
role := "entry"
|
|
if os.Getenv("CTHULHU_NATIVE_FIREFOX") == "1" {
|
|
role = "combo box"
|
|
}
|
|
wait(role, "")
|
|
settle(false)
|
|
for i := 0; i < 2; i++ {
|
|
keys("ctrl+t")
|
|
request := wait("page tab", "New Tab")
|
|
if request.Cause != atspi.SelectedStateEvent && request.Cause != atspi.SelectionChangedEvent {
|
|
t.Fatalf("new tab did not use selected state: %#v", request)
|
|
}
|
|
settle(true)
|
|
}
|
|
keys("ctrl+shift+Tab")
|
|
wait("page tab", "New Tab")
|
|
settle(true)
|
|
keys("ctrl+w")
|
|
wait("page tab", "New Tab")
|
|
settle(true)
|
|
keys("ctrl+w")
|
|
wait("page tab", "Cthulhu")
|
|
settle(true)
|
|
keys("ctrl+l")
|
|
keys("x")
|
|
wait(role, "x")
|
|
settle(false)
|
|
keys("BackSpace")
|
|
wait(role, "x")
|
|
}
|