Files
cthulhu/internal/app/browser_tabs_native_test.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")
}