2026-10-24 10:37:46 +02:00
|
|
|
package notify
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-24 12:10:49 +02:00
|
|
|
// TestMain cuts the tests off from the developer's real session bus so they
|
|
|
|
|
// observe the notify-send fallback instead of popping real notifications.
|
|
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
|
os.Setenv("DBUS_SESSION_BUS_ADDRESS", "unix:path=/nonexistent/talk-test-bus")
|
|
|
|
|
os.Exit(m.Run())
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestShowCallsNotifySend: on Linux, Show goes through notify-send; here a
|
|
|
|
|
// fake in PATH that records its arguments (a real process, no mock
|
|
|
|
|
// framework). Title and text arrive unchanged.
|
|
|
|
|
func TestShowCallsNotifySend(t *testing.T) {
|
2026-10-24 10:37:46 +02:00
|
|
|
if runtime.GOOS != "linux" {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Skip("the notify-send path exists only on Linux")
|
2026-10-24 10:37:46 +02:00
|
|
|
}
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
seen := filepath.Join(dir, "seen.txt")
|
|
|
|
|
script := "#!/bin/sh\nprintf '%s\\n' \"$@\" > \"" + seen + "\"\n"
|
|
|
|
|
if err := os.WriteFile(filepath.Join(dir, "notify-send"), []byte(script), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", dir)
|
|
|
|
|
|
|
|
|
|
if !Available() {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatal("Available = false with fake in PATH")
|
2026-10-24 10:37:46 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if err := Show(context.Background(), "quiet hours: build done"); err != nil {
|
2026-10-24 10:37:46 +02:00
|
|
|
t.Fatalf("Show: %v", err)
|
|
|
|
|
}
|
|
|
|
|
b, err := os.ReadFile(seen)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
args := strings.Split(strings.TrimSpace(string(b)), "\n")
|
2026-10-24 11:15:35 +02:00
|
|
|
if args[len(args)-1] != "quiet hours: build done" || args[len(args)-2] != Title {
|
|
|
|
|
t.Errorf("notify-send arguments = %q, want ... %q %q", args, Title, "quiet hours: build done")
|
2026-10-24 10:37:46 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-24 11:15:35 +02:00
|
|
|
// TestShowWithoutTool: a missing tool is a clear error.
|
|
|
|
|
func TestShowWithoutTool(t *testing.T) {
|
2026-10-24 10:37:46 +02:00
|
|
|
t.Setenv("PATH", t.TempDir())
|
|
|
|
|
if Available() {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Fatal("Available = true without tool")
|
2026-10-24 10:37:46 +02:00
|
|
|
}
|
|
|
|
|
if err := Show(context.Background(), "x"); err == nil {
|
2026-10-24 11:15:35 +02:00
|
|
|
t.Error("Show without tool: no error")
|
2026-10-24 10:37:46 +02:00
|
|
|
}
|
2026-10-24 11:15:35 +02:00
|
|
|
if !strings.Contains(Describe(), "not found") {
|
2026-10-24 10:37:46 +02:00
|
|
|
t.Errorf("Describe = %q", Describe())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAppleString(t *testing.T) {
|
2026-10-24 11:15:35 +02:00
|
|
|
if got := appleString(`say "hello" \ bye`); got != `"say \"hello\" \\ bye"` {
|
2026-10-24 10:37:46 +02:00
|
|
|
t.Errorf("appleString = %s", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-25 12:43:14 +01:00
|
|
|
|
|
|
|
|
// TestToastIdentity: under talk's own AppUserModelID the toast header names
|
|
|
|
|
// talk already, so the scripts carry only the body; under the PowerShell
|
|
|
|
|
// fallback identity a first line names talk.
|
|
|
|
|
func TestToastIdentity(t *testing.T) {
|
|
|
|
|
a := Announcement{Text: "build done", CancelLabel: "Cancel", CancelURI: "talk:cancel?id=1", Tag: "1"}
|
|
|
|
|
own := cancelToastScript(a, "talk-1", AppID)
|
|
|
|
|
if !strings.Contains(own, "CreateToastNotifier('"+AppID+"')") || strings.Contains(own, "<text>"+Title+"</text>") {
|
|
|
|
|
t.Errorf("own identity script wrong:\n%s", own)
|
|
|
|
|
}
|
|
|
|
|
fallback := cancelToastScript(a, "talk-1", powerShellAppID)
|
|
|
|
|
if !strings.Contains(fallback, "CreateToastNotifier('"+powerShellAppID+"')") || !strings.Contains(fallback, "<text>"+Title+"</text>") {
|
|
|
|
|
t.Errorf("fallback script wrong:\n%s", fallback)
|
|
|
|
|
}
|
|
|
|
|
plain := toastScript("quiet", AppID)
|
|
|
|
|
if strings.Contains(plain, "<text>"+Title+"</text>") || !strings.Contains(plain, "<text>$msg</text>") {
|
|
|
|
|
t.Errorf("plain own-identity script wrong:\n%s", plain)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(toastScript("quiet", powerShellAppID), "<text>"+Title+"</text>") {
|
|
|
|
|
t.Error("plain fallback script lacks the talk line")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestUseIconOutsideWindows: UseIcon remembers the file and the freedesktop
|
|
|
|
|
// notification carries it as app_icon; without one a theme icon is used.
|
|
|
|
|
func TestUseIconOutsideWindows(t *testing.T) {
|
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
|
t.Skip("registry path on Windows")
|
|
|
|
|
}
|
|
|
|
|
setIconPath("")
|
|
|
|
|
if appIcon() != "audio-speakers" {
|
|
|
|
|
t.Errorf("appIcon without file = %q", appIcon())
|
|
|
|
|
}
|
|
|
|
|
if err := UseIcon("/x/talk.png"); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { setIconPath("") })
|
|
|
|
|
if appIcon() != "/x/talk.png" {
|
|
|
|
|
t.Errorf("appIcon = %q", appIcon())
|
|
|
|
|
}
|
|
|
|
|
if ownIdentity() {
|
|
|
|
|
t.Error("ownIdentity true outside Windows")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestSetup: the icon lands in dir and is used; with no exe the protocol
|
|
|
|
|
// step is skipped (outside Windows it is a no-op anyway).
|
|
|
|
|
func TestSetup(t *testing.T) {
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
|
if err := Setup(dir, ""); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() { setIconPath("") })
|
|
|
|
|
want := filepath.Join(dir, "talk.png")
|
|
|
|
|
if _, err := os.Stat(want); err != nil {
|
|
|
|
|
t.Fatalf("icon not installed: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if runtime.GOOS != "windows" && appIcon() != want {
|
|
|
|
|
t.Errorf("appIcon = %q, want %q", appIcon(), want)
|
|
|
|
|
}
|
|
|
|
|
if err := Setup("", ""); err == nil {
|
|
|
|
|
t.Error("Setup without dir succeeded")
|
|
|
|
|
}
|
|
|
|
|
}
|