Files
mesh-catalog/modules/rofi/cmd/rofi-tools/rofi_test.go
T
jochen f0f0a79623 rofi: the launcher as a module, claiming node-launcher and serving menu (hq ADR 0208)
Places the seat's dmenu-compatible command, the launcher and power menu, its
themes in the decided faces, and its key bindings as an i3 drop-in; Go tools
menu, applications, themes and run.
2026-10-04 13:15:39 +02:00

197 lines
7.5 KiB
Go

package main
import (
"errors"
"os"
"os/exec"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
)
const nobody = 4194400
func withSession(t *testing.T) string {
t.Helper()
root := fakeMachine(t)
fakeProcess(t, nobody, "i3", "DISPLAY=:1", "XDG_SESSION_ID=2")
return root
}
func TestTheMenuAnswersTheChosenLineAndItsIndex(t *testing.T) {
withSession(t)
bin := fakeBinaries(t, map[string]string{"rofi": `echo "$* DISPLAY=$DISPLAY" > "$LOG"; cat > "$LOG.in"; echo "second"`})
t.Setenv("LOG", filepath.Join(bin, "log"))
q, err := menuQuestion(map[string]any{"items": []any{"first", "second"}, "prompt": "Pick", "message": "why"})
if err != nil {
t.Fatal(err)
}
got, err := Menu(q)
if err != nil || got.Chosen != "second" || got.Index != 1 || got.Cancelled || got.Custom {
t.Fatalf("%+v, %v", got, err)
}
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
if strings.TrimSpace(string(asked)) != "-dmenu -i -p Pick -format s -mesg why -no-custom DISPLAY=:1" {
t.Fatalf("rofi was asked: %s", asked)
}
in, _ := os.ReadFile(filepath.Join(bin, "log.in"))
if string(in) != "first\nsecond\n" {
t.Fatalf("the choices, one per line: %q", in)
}
}
func TestClosingTheMenuOrNotAnsweringIsACancelNotAnError(t *testing.T) {
withSession(t)
fakeBinaries(t, map[string]string{"rofi": `exit 1`})
got, err := Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", Timeout: 5e9})
if err != nil || !got.Cancelled || got.Index != -1 {
t.Fatalf("closed: %+v, %v", got, err)
}
fakeBinaries(t, map[string]string{"rofi": `sleep 30`})
got, err = Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", Timeout: 2e8})
if err != nil || !got.Cancelled || !got.TimedOut {
t.Fatalf("not answered: %+v, %v", got, err)
}
fakeBinaries(t, map[string]string{"rofi": `echo typed`})
got, err = Menu(MenuQuestion{Items: []string{"a"}, Prompt: "p", AllowCustom: true, Timeout: 5e9})
if err != nil || got.Chosen != "typed" || !got.Custom || got.Index != -1 {
t.Fatalf("typed: %+v, %v", got, err)
}
}
func TestAMenuQuestionIsBoundedAndOneLinePerChoice(t *testing.T) {
for _, bad := range []map[string]any{
{},
{"items": []any{}},
{"items": []any{"two\nlines"}},
{"items": []any{"a"}, "timeout_seconds": float64(60)},
{"items": []any{3.0}},
} {
if _, err := menuQuestion(bad); err == nil {
t.Errorf("accepted %v", bad)
}
}
q, err := menuQuestion(map[string]any{"items": []any{"a"}})
if err != nil || q.Prompt != "Choose" || q.Timeout.Seconds() != menuDefault {
t.Fatalf("defaults: %+v, %v", q, err)
}
}
func TestWithoutASessionTheMenuSaysSo(t *testing.T) {
fakeMachine(t)
if _, err := Menu(MenuQuestion{Items: []string{"a"}, Timeout: 1e9}); !errors.Is(err, ErrNoSession) {
t.Fatal(err)
}
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestApplicationsAreTheDesktopEntriesTheAccountsWinning(t *testing.T) {
root := t.TempDir()
own, system := filepath.Join(root, "own"), filepath.Join(root, "system")
writeFile(t, filepath.Join(system, "firefox.desktop"), "[Desktop Entry]\nType=Application\nName=Firefox\nExec=firefox %u\nCategories=Network;WebBrowser;\n[Desktop Action new]\nName=New Window\nExec=firefox --new-window\n")
writeFile(t, filepath.Join(own, "firefox.desktop"), "[Desktop Entry]\nType=Application\nName=Firefox (mine)\nExec=firefox -P work %u\n")
writeFile(t, filepath.Join(system, "kde", "konsole.desktop"), "[Desktop Entry]\nType=Application\nName=Konsole\nExec=konsole\nTerminal=false\nKeywords=terminal;shell;\n")
writeFile(t, filepath.Join(system, "hidden.desktop"), "[Desktop Entry]\nType=Application\nName=Helper\nExec=helper\nNoDisplay=true\n")
writeFile(t, filepath.Join(system, "link.desktop"), "[Desktop Entry]\nType=Link\nName=A link\nURL=https://example.org\n")
got := Applications([]string{own, system}, "", false, 10)
var names []string
for _, a := range got.Applications {
names = append(names, a.ID+"="+a.Name)
}
if !reflect.DeepEqual(names, []string{"firefox.desktop=Firefox (mine)", "kde-konsole.desktop=Konsole"}) {
t.Fatalf("%v", names)
}
if got := Applications([]string{own, system}, "SHELL", false, 10); got.Count != 1 || got.Applications[0].Name != "Konsole" {
t.Fatalf("by keyword: %+v", got)
}
if got := Applications([]string{own, system}, "", true, 1); got.Count != 3 || !got.Truncated || len(got.Applications) != 1 {
t.Fatalf("hidden and limited: %+v", got)
}
}
func TestThemesAreListedOnceWithTheConfiguredOne(t *testing.T) {
root := t.TempDir()
mine, system := filepath.Join(root, "mine"), filepath.Join(root, "system")
writeFile(t, filepath.Join(mine, "mesh.rasi"), "*{}")
writeFile(t, filepath.Join(system, "mesh.rasi"), "*{}")
writeFile(t, filepath.Join(system, "Arc.rasi"), "*{}")
conf, _ := os.ReadFile(filepath.Join("..", "..", "files", "config.rasi"))
writeFile(t, filepath.Join(root, "config.rasi"), string(conf))
got := Themes([]themeDir{{mine, "mine"}, {system, "system"}}, filepath.Join(root, "config.rasi"))
if got.Current != "mesh" || len(got.Themes) != 2 || got.Themes[1].Source != "mine" {
t.Fatalf("%+v", got)
}
}
func TestExecIsSplitAsTheDesktopEntrySpecQuotesIt(t *testing.T) {
for in, want := range map[string][]string{
"firefox %u": {"firefox"},
`sh -c "echo \"a b\"" %F`: {"sh", "-c", `echo "a b"`},
"printf 100%% --flag": {"printf", "100%", "--flag"},
`"/opt/My App/run" --x`: {"/opt/My App/run", "--x"},
} {
got, err := execWords(in)
if err != nil || !reflect.DeepEqual(got, want) {
t.Errorf("%s: %q, %v", in, got, err)
}
}
for _, bad := range []string{`"open`, "%u"} {
if _, err := execWords(bad); err == nil {
t.Errorf("%s was accepted", bad)
}
}
}
func TestRunStartsAnApplicationUnderTheAccountsServiceManager(t *testing.T) {
root := withSession(t)
writeFile(t, filepath.Join(root, "home", ".local", "share", "applications", "htop.desktop"),
"[Desktop Entry]\nType=Application\nName=htop\nExec=htop\nTerminal=true\n")
t.Setenv("XDG_DATA_DIRS", filepath.Join(root, "none"))
// The account's runtime directory, through which its service manager is reached.
if err := os.MkdirAll(filepath.Join(runUserDir, strconv.Itoa(os.Getuid())), 0o700); err != nil {
t.Fatal(err)
}
bin := fakeBinaries(t, map[string]string{"systemd-run": `echo "$*" > "$LOG"`, "systemctl": `true`})
t.Setenv("LOG", filepath.Join(bin, "log"))
got, err := Run("htop.desktop", nil)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got.Argv, []string{"rofi-sensible-terminal", "-e", "htop"}) || !strings.HasPrefix(got.Unit, "rofi-run-") {
t.Fatalf("%+v", got)
}
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
if !strings.Contains(string(asked), "--setenv=DISPLAY=:1 --setenv=XDG_SESSION_ID=2 -- rofi-sensible-terminal -e htop") {
t.Fatalf("systemd-run was asked: %s", asked)
}
if _, err := Run("", nil); err == nil {
t.Fatal("nothing to run was accepted")
}
if _, err := Run("nope.desktop", nil); err == nil {
t.Fatal("a missing entry was accepted")
}
}
func TestTheDmenuCommandHandsRofiWhatItUnderstands(t *testing.T) {
fakeBinaries(t, map[string]string{"rofi": `for a in "$@"; do printf '[%s]' "$a"; done`})
shim, _ := filepath.Abs(filepath.Join("..", "..", "files", "bin", "dmenu"))
out, err := exec.Command(shim, "-b", "-fn", "Mono 10", "-l", "8", "-p", "pick one", "-nb", "#000", "-i").Output()
if err != nil {
t.Fatal(err)
}
if string(out) != "[-dmenu][-l][8][-p][pick one][-i]" {
t.Fatalf("rofi was handed %s", out)
}
}