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) } }