// 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" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "github.com/godbus/dbus/v5" ) type RuntimeKeyboardMonitor struct{ ready chan struct{} } type RuntimeKeyGrab struct{ Symbol, Modifiers uint32 } func (*RuntimeKeyboardMonitor) WatchKeyboard() *dbus.Error { return nil } func (monitor *RuntimeKeyboardMonitor) SetKeyGrabs([]uint32, []RuntimeKeyGrab) *dbus.Error { select { case monitor.ready <- struct{}{}: default: } return nil } func TestNativeWaylandRuntimeKeyboardPreferencesAndQuit(t *testing.T) { if os.Getenv("CTHULHU_NATIVE_DBUS_TEST") != "1" { t.Skip("requires private dbus-run-session") } t.Setenv("XDG_SESSION_TYPE", "wayland") t.Setenv("DISPLAY", "") connection, err := dbus.ConnectSessionBus() if err != nil { t.Fatal(err) } defer connection.Close() const managerName = "org.freedesktop.a11y.Manager" const managerPath = dbus.ObjectPath("/org/freedesktop/a11y/Manager") const monitorInterface = "org.freedesktop.a11y.KeyboardMonitor" monitor := &RuntimeKeyboardMonitor{ready: make(chan struct{}, 1)} if err := connection.Export(monitor, managerPath, monitorInterface); err != nil { t.Fatal(err) } if result, err := connection.RequestName(managerName, dbus.NameFlagDoNotQueue); err != nil || result != dbus.RequestNameReplyPrimaryOwner { t.Fatalf("keyboard manager: %v %v", result, err) } speaker := &fakeSpeaker{spoken: make(chan string, 32)} adapter := &fakeAdapter{events: make(chan atspi.Event, 1)} preferences := make(chan struct{}, 1) ctx, cancel := context.WithCancel(context.Background()) defer cancel() completed := make(chan error, 1) go func() { completed <- Run(ctx, Options{ Logger: slog.New(slog.DiscardHandler), newSpeaker: func() Speaker { return speaker }, newSound: func() soundService { return silentSound{} }, newAdapter: func() Adapter { return adapter }, onPreferences: func() { preferences <- struct{}{} }, }) }() select { case <-monitor.ready: case <-time.After(3 * time.Second): t.Fatal("runtime did not select shared keyboard backend") } key := func(symbol uint32, code uint16, released bool) { t.Helper() if err := connection.Emit(managerPath, monitorInterface+".KeyEvent", released, uint32(0), symbol, uint32(0), code); err != nil { t.Fatal(err) } } key(0xff63, 118, false) key(' ', 65, false) key(' ', 65, true) key(0xff63, 118, true) select { case <-preferences: case err := <-completed: t.Fatalf("runtime exited before preferences: %v", err) case <-time.After(3 * time.Second): t.Fatal("Wayland preferences shortcut was not dispatched") } key(0xff63, 118, false) key('q', 24, false) key(0xff63, 118, true) key('q', 24, true) select { case err := <-completed: if err != nil { t.Fatal(err) } case <-time.After(4 * time.Second): t.Fatal("Wayland quit shortcut did not stop runtime") } if !adapter.stopped || !speaker.closed { t.Fatal("Wayland runtime shutdown leaked services") } }