// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "sync/atomic" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/braille" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/input" "git.stormux.org/storm/cthulhu/internal/interaction" "git.stormux.org/storm/cthulhu/internal/review" ) type uiTestKeys struct{ session atomic.Uint64 } func (keys *uiTestKeys) InternalUISession() uint64 { return keys.session.Load() } func (keys *uiTestKeys) InternalUIActive() bool { return keys.session.Load() != 0 } func (keys *uiTestKeys) BeginInternalUI(context.Context) (uint64, error) { keys.session.Store(1) return 1, nil } func (keys *uiTestKeys) EndInternalUI(_ context.Context, id uint64) error { keys.session.CompareAndSwap(id, 0) return nil } type uiTestActions struct { keys *uiTestKeys invoked chan string object atspi.ObjectID } func (source *uiTestActions) ObjectOfInterest(context.Context) (review.Interest, error) { return review.Interest{Object: source.object}, nil } func (source *uiTestActions) AccessibleActions(_ context.Context, object atspi.ObjectID) (atspi.ActionSet, error) { return atspi.ActionSet{Target: atspi.ActionTarget{Object: object}, Actions: []atspi.AccessibleAction{{Name: "click", Label: "Click"}, {Name: "press", Label: "Press"}}}, nil } func (source *uiTestActions) PerformAccessibleAction(_ context.Context, target atspi.ActionTarget, name string) error { if source.keys.InternalUISession() != 0 || target.Object != source.object { source.invoked <- "wrong target or capture retained" } else { source.invoked <- name } return nil } func TestInternalUIActionsAndFormUseSameHost(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() keys := &uiTestKeys{} keys.session.Store(1) source := &uiTestActions{keys: keys, object: atspi.NewObjectID(":1.42", "/button"), invoked: make(chan string, 1)} output := &ocrTestOutput{speech: make(chan speechRequest, 16)} ui := &internalUI{keys: keys, actions: source, interest: source, speech: output, gate: &focusGate{}, events: make(chan input.CommandEvent, 16), requests: make(chan internalUIRequest, 1), done: make(chan struct{})} go ui.run(ctx) defer func() { cancel(); <-ui.done }() ui.Handle(input.CommandEvent{Command: input.ShowActions, UISession: 1}) select { case message := <-output.speech: if message.Cause != "internal-ui" || message.Text != "Actions. Click" { t.Fatal(message) } case <-ctx.Done(): t.Fatal("menu did not open") } ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff54}}) ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff0d}}) select { case action := <-source.invoked: if action != "press" { t.Fatal(action) } case <-ctx.Done(): t.Fatal("action did not run") } select { case message := <-output.speech: if message.Text != "Press" { t.Fatalf("navigation repeated title: %q", message.Text) } case <-ctx.Done(): t.Fatal("selection was not announced") } result, err := ui.Open(ctx, interaction.Form{Title: "Example", Elements: []interaction.Element{{ID: "entry", Label: "Code", Kind: interaction.Entry}}}) if err != nil { t.Fatal(err) } for keys.InternalUISession() == 0 { if ctx.Err() != nil { t.Fatal("form did not open") } time.Sleep(time.Millisecond) } ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Text: "é", Chord: input.CommandChord{Symbol: 0xe9}}) ui.Handle(input.CommandEvent{Command: input.InternalUIKey, UISession: 1, Chord: input.CommandChord{Symbol: 0xff0d}}) select { case submitted := <-result: if submitted.Activated != "entry" || submitted.Values["entry"].Text != "é" { t.Fatalf("form=%+v", submitted) } case <-ctx.Done(): t.Fatal("form did not submit") } } func TestInternalUIBrailleRestoresNewestApplicationFrame(t *testing.T) { worker := &brailleWorker{wake: make(chan struct{}, 1), cancel: func() {}, connected: true} worker.EnqueueFrame(braille.Frame{Text: "Application", Cursor: 2}) worker.SetInternalUIFrame(&braille.Frame{Text: "Code, edit: abc", Cursor: 14}) worker.EnqueueFrame(braille.Frame{Text: "Newest application", Cursor: 4}) if worker.current().Text != "Code, edit: abc" { t.Fatal("application overwrote UI") } options := config.DefaultSettings().Braille options.FlashMessages = true if worker.Flash("Background", options) { t.Fatal("background flash overwrote UI") } worker.SetInternalUIFrame(nil) if worker.current().Text != "Newest application" || worker.current().Cursor != 4 { t.Fatal("latest application frame not restored") } }