// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. package notify import "testing" type event struct { id uint32 reason uint32 } func newTestStore() (*Store, *[]event) { emitted := &[]event{} s := NewStore( func(id, reason uint32) { *emitted = append(*emitted, event{id, reason}) }, func(live, history []Popup) {}, nil, ) return s, emitted } func popup(app string) *Popup { return &Popup{App: app, Urgency: Normal, Created: 1} } func TestAddAssignsIdsFromOne(t *testing.T) { s, _ := newTestStore() a, replaced := s.Add(popup("a"), "", 0) b, _ := s.Add(popup("b"), "", 0) if replaced { t.Fatal("first add reported replaced") } if a != 1 || b != 2 { t.Errorf("ids = %d, %d, want 1, 2", a, b) } } func TestReplaceByIDReusesTheIDAndEmitsNothing(t *testing.T) { s, emitted := newTestStore() id, _ := s.Add(popup("a"), "", 0) *emitted = nil again, replaced := s.Add(popup("a2"), "", id) if !replaced { t.Fatal("replace by id not reported") } if again != id { t.Errorf("replace id = %d, want %d", again, id) } if len(*emitted) != 0 { t.Errorf("replace emitted %v, want none", *emitted) } } func TestReplaceByStackTag(t *testing.T) { s, _ := newTestStore() first, _ := s.Add(popup("mail"), "mail-account", 0) second, replaced := s.Add(popup("mail"), "mail-account", 0) if !replaced || second != first { t.Errorf("stack replace = id %d replaced %v, want id %d replaced true", second, replaced, first) } } func TestExpireClosesOnceAndKeepsTheEntry(t *testing.T) { s, emitted := newTestStore() id, _ := s.Add(popup("a"), "", 0) s.Expire(id) s.Expire(id) if len(*emitted) != 1 || (*emitted)[0].reason != 1 { t.Fatalf("emitted = %v, want one reason 1", *emitted) } if s.Actionable(id) { t.Error("expired entry still actionable") } } func TestDismissFilesHistoryAndEmitsDismissed(t *testing.T) { s, emitted := newTestStore() id, _ := s.Add(popup("a"), "", 0) s.Dismiss(id, 2) if len(*emitted) != 1 || (*emitted)[0].reason != 2 { t.Fatalf("emitted = %v, want one reason 2", *emitted) } if s.Actionable(id) { t.Error("dismissed entry still live") } } func TestDismissAfterExpireEmitsNothingMore(t *testing.T) { s, emitted := newTestStore() id, _ := s.Add(popup("a"), "", 0) s.Expire(id) *emitted = nil s.Dismiss(id, 2) if len(*emitted) != 0 { t.Errorf("dismiss after expire emitted %v, want none", *emitted) } } func TestQueueCapEvictsToHistory(t *testing.T) { s, emitted := newTestStore() for i := 0; i < 21; i++ { s.Add(popup("a"), "", 0) } if len(*emitted) == 0 { t.Fatal("eviction emitted nothing") } history := s.historySnapshot() if len(history) != 1 { t.Fatalf("history length = %d, want 1", len(history)) } if !history[0].Closed { t.Error("evicted entry not marked closed in history") } } func TestHistoryRingCapsAtTwenty(t *testing.T) { s, _ := newTestStore() for i := 0; i < 30; i++ { id, _ := s.Add(popup("a"), "", 0) s.Dismiss(id, 2) } if got := len(s.historySnapshot()); got != 20 { t.Errorf("history length = %d, want 20", got) } } func TestStoreRemovesImageOnDismissAndExpire(t *testing.T) { var removed []string s := NewStore(func(uint32, uint32) {}, func(_, _ []Popup) {}, func(p string) { removed = append(removed, p) }) id, _ := s.Add(&Popup{Image: "/run/img/1.png"}, "", 0) s.SetImage(id, "/run/img/other.png") s.Dismiss(id, 2) if len(removed) != 1 || removed[0] != "/run/img/other.png" { t.Fatalf("dismiss removed %v", removed) } } func TestStoreRemovesImageOnReplace(t *testing.T) { var removed []string s := NewStore(func(uint32, uint32) {}, func(_, _ []Popup) {}, func(p string) { removed = append(removed, p) }) s.Add(&Popup{Image: "/run/img/old.png"}, "tag", 0) id, _ := s.Add(&Popup{Image: "/run/img/new.png"}, "tag", 0) _ = id if len(removed) != 1 || removed[0] != "/run/img/old.png" { t.Fatalf("replace removed %v", removed) } }