package main import ( "reflect" "strings" "testing" ) // dunst's shape (novox/hq ADR 0208): it claims node-notifier serving send and history, owns its // dunstrc and the drop-in directory other modules' rules go in, and starts nothing — D-Bus starts it // on the first notification; the mesh only tells a running one to read its file again. It draws only once a notification arrives, through the bus's // activation, so it requires no display of its own. func TestItClaimsTheNotifierSeatServingSendAndHistory(t *testing.T) { m := readManifest(t) if m.Module != "dunst" || m.Seats != nil { t.Fatalf("module %q declares seats %v", m.Module, m.Seats) } if !reflect.DeepEqual(m.Claims, []claim{{Name: "node-notifier", Scope: "node", Serves: []string{"send", "history"}}}) { t.Fatalf("claims: %+v", m.Claims) } // rofi, the context menu the dunstrc calls by name, is the rofi module's package: two modules on one // machine may not declare the same package, and the rofi module holds node-launcher on every // machine dunst serves. if present, absent := m.packages(); !reflect.DeepEqual(present, []string{"dunst", "libnotify"}) || absent != nil { t.Fatalf("packages: %v, absent %v", present, absent) } } func TestItOwnsItsFileAndTheDropInDirectory(t *testing.T) { m := readManifest(t) m.fromTemplate(t, "configuration", "files/dunstrc") if p := m.resource(t, "configuration")["path"]; p != "${machine:account-home}/.config/dunst/dunstrc" { t.Fatalf("path: %v", p) } if d := m.resource(t, "dropins"); d["type"] != "directory" || d["path"] != "${machine:account-home}/.config/dunst/dunstrc.d" { t.Fatalf("drop-ins: %v", d) } for _, r := range m.Resources { if p, _ := r["path"].(string); strings.Contains(p, "dunstrc.d/") { t.Fatalf("a rule of another module's: %v", r) } } } func TestTheFileIsTheDecidedOneAndCallsRofi(t *testing.T) { m := readManifest(t) c := m.text(t, "configuration") for _, want := range []string{"origin = top-right", "transparency = 0", "corner_radius = 0", "font = Inter ${config:font-size}", "width = ${config:width}", "dmenu = rofi -dmenu -no-custom -p dunst"} { if !strings.Contains(c, " "+want+"\n") { t.Errorf("lacks %q", want) } } // Not the node-launcher's `dmenu` in ~/.local/bin: dunst's service manager has no such directory on // its PATH, so that menu never opened. for _, never := range []string{"/usr/bin/dmenu", "dmenu = dmenu", "Hack", "icon_path", "/home/"} { if strings.Contains(c, never) { t.Errorf("names %q", never) } } } // A tap chooses (the operator's touchpads have no middle button): the left click is dunst's do_action // and then close_current, so a notification with one action runs it, one with several opens the menu // of them (dunst holds it open until the menu answers, and an Escape leaves it shown), and one without // actions closes. Every other click is as it was. func TestATapChoosesTheNotificationsAction(t *testing.T) { m := readManifest(t) c := m.text(t, "configuration") for _, want := range []string{"mouse_left_click = do_action, close_current", "mouse_middle_click = do_action, close_current", "mouse_right_click = close_all"} { if !strings.Contains(c, " "+want+"\n") { t.Errorf("lacks %q", want) } } if strings.Count(c, "mouse_left_click") != 1 { t.Errorf("sets mouse_left_click more than once") } } // The key that chooses from the keyboard is a line i3 places from this contribution (novox/hq ADR // 0212): a binding only, never a start, so the bus stays the only thing that starts dunst. func TestTheMenuKeyIsABindingI3Places(t *testing.T) { m := readManifest(t) m.i3LinesAreSource(t, "files/i3/50-dunst.conf") lines := m.i3Lines() if !strings.Contains(lines, "bindsym $mod+n exec --no-startup-id dunstctl context\n") { t.Fatalf("no key for the context menu: %s", lines) } for _, l := range strings.Split(lines, "\n") { if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "#") && !strings.HasPrefix(l, "bindsym ") { t.Errorf("a line that is not a key: %q", l) } } } // The font size and the width are config values with today's values as their defaults (novox/hq ADR // 0348 §4): numbers with their unit said and their bounds, the font size falling back to the mesh's // predefined font-size (ADR 0348 §2), so a machine with a denser screen sets its own and every other // machine's file is unchanged. Neither is trusted: the file says "trusted": false. func TestTheFontSizeAndWidthAreConfigValuesWithTodaysDefaults(t *testing.T) { m := readManifest(t) want := map[string]float64{"font-size": 10, "width": 250} if len(m.Config) != len(want) { t.Fatalf("config: %+v", m.Config) } for key, value := range want { s, ok := m.Config[key] if !ok || s.Type != "number" || s.Default != value || s.Required || s.Trusted || strings.TrimSpace(s.Description) == "" || s.Minimum == nil || s.Maximum == nil { t.Errorf("%s: %+v, want an untrusted number defaulting to %v, with a description and bounds", key, s, value) } } if p := m.Config["font-size"].Predefined; p != "font-size" { t.Errorf("the font size falls back to %q, want the predefined font-size", p) } // Filled with the defaults, the file is the one every machine had before it took config values. c := m.text(t, "configuration") c = strings.ReplaceAll(c, "${config:font-size}", "10") c = strings.ReplaceAll(c, "${config:width}", "250") for _, want := range []string{"font = Inter 10", "width = 250"} { if !strings.Contains(c, " "+want+"\n") { t.Errorf("filled with the defaults, lacks %q", want) } } if strings.Contains(c, "${config:") || strings.Contains(c, "${setting:") { t.Errorf("asks for a config value it gives no default: %s", c) } } func TestNothingStartsItButTheBus(t *testing.T) { m := readManifest(t) if m.Shell != nil { t.Fatalf("a session start: %+v", m.Shell) } for _, r := range m.Resources { // The one unit it names is the package's own dunst.service, without a state: its lifecycle stays // the bus's, and the mesh only reloads it when it is running and its file changed (ADR 0117). if r["type"] == "process" || (r["type"] == "service" && (r["state"] != nil || r["boot"] != nil)) { t.Fatalf("a unit: %v", r) } if p, _ := r["path"].(string); strings.Contains(p, "autostart") || strings.Contains(p, "i3/config.d") { t.Fatalf("a start: %v", r) } } } func TestTheToolsAgreeWithTheManifest(t *testing.T) { m := readManifest(t) checkTheToolsAgree(t, m) checkNoSecretsOrInstallationNames(t) } // No file resource carries its text inline (novox/hq ADR 0349 §2). func TestEveryFileTakesItsTextFromATemplate(t *testing.T) { readManifest(t).everyFileIsATemplate(t) } // dunst moves first (design 57 P6, issue 672): marked moved, its dunstrc is filled on the node by the provisioner's // client from its template, and reloaded as the file declares. func TestItIsHandedToTheProvisioner(t *testing.T) { m := readManifest(t) if !m.Moved { t.Fatal("dunst is not marked moved") } m.fromTemplate(t, "configuration", "files/dunstrc") if on := m.resource(t, "notifier")["reload-on"]; !reflect.DeepEqual(on, []any{"configuration"}) { t.Fatalf("the notifier is not reloaded on its dunstrc: %v", on) } }