// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "log/slog" "testing" "time" "git.stormux.org/storm/cthulhu/internal/atspi" "git.stormux.org/storm/cthulhu/internal/notification" ) func TestNotificationProvidersAndHistoryMatchReferenceBehavior(t *testing.T) { tests := []struct { name string event atspi.Event expectedSpeech []string expectedHistory string }{ { name: "xfce notification daemon labels", event: notificationSnapshot("xfce4-notifyd", atspi.WindowCreateEvent, atspi.NewSnapshot( "/notification", "", "frame", "", "Mail arrived New message", )), expectedSpeech: []string{"Notification Mail arrived New message"}, expectedHistory: "Notification Mail arrived New message", }, { name: "notify-osd name without value announces prefix and message", event: notificationSnapshot("notify-osd", atspi.AccessibleNameEvent, atspi.NewSnapshot( "/notification", "Volume", "notification", "at maximum", "", )), expectedSpeech: []string{"Notification", "Volume at maximum"}, expectedHistory: "Volume at maximum", }, { name: "notify-osd gauge value is not saved", event: gaugeSnapshot(), expectedSpeech: []string{"55"}, expectedHistory: "", }, { name: "generic accessible notification is saved", event: showingNotification("Mail", "You have one unread message", true), expectedSpeech: []string{"Mail, notification, You have one unread message"}, expectedHistory: "Mail You have one unread message", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { presenter := notification.NewPresenter() speech, entries := processNotificationEvent(t, test.event, presenter) if len(speech) != len(test.expectedSpeech) { t.Fatalf("speech count = %d, want %d: %q", len(speech), len(test.expectedSpeech), speech) } for index, expected := range test.expectedSpeech { if speech[index] != expected { t.Fatalf("speech %d = %q, want %q", index, speech[index], expected) } } if test.expectedHistory == "" { if len(entries) != 0 { t.Fatalf("history length = %d, want 0", len(entries)) } return } if len(entries) != 1 { t.Fatalf("history length = %d, want 1", len(entries)) } if entries[0].Message != test.expectedHistory { t.Fatalf("history = %q, want %q", entries[0].Message, test.expectedHistory) } }) } } func TestHiddenGenericNotificationIsNotSaved(t *testing.T) { event := showingNotification("Mail", "You have one unread message", false) presenter := notification.NewPresenter() _, entries := processNotificationEvent(t, event, presenter) if len(entries) != 0 { t.Fatalf("history length = %d, want 0", len(entries)) } } func showingNotification(name, text string, showing bool) atspi.Event { event := notificationSnapshot("Application", atspi.ShowingStateEvent, atspi.NewSnapshot("/notification", name, "notification", "", text)) if showing { event.Detail1 = 1 } return event } func notificationSnapshot(application, eventType string, snapshot atspi.Snapshot) atspi.Event { snapshot.ApplicationName = application return atspi.NewEvent(eventType, snapshot) } func gaugeSnapshot() atspi.Event { snapshot := atspi.NewSnapshot("/notification", "Volume", "notification", "", "") snapshot.ValueKnown = true snapshot.CurrentValue = 55 return notificationSnapshot("notify-osd", atspi.ValueChangedEvent, snapshot) } func processNotificationEvent(t *testing.T, event atspi.Event, presenter *notification.Presenter) ([]string, []notification.Entry) { t.Helper() speech := &fakeSpeechQueue{spoken: make(chan string, 8)} adapter := &fakeAdapter{events: make(chan atspi.Event, 1)} adapter.events <- event ctx, cancel := context.WithCancel(context.Background()) defer cancel() completed := make(chan error, 1) go func() { completed <- processEventsWithInput( ctx, slog.New(slog.DiscardHandler), speech, adapter, nil, nil, 0, nil, nil, nil, presenter, ) }() var messages []string for { select { case message := <-speech.spoken: messages = append(messages, message) case <-time.After(50 * time.Millisecond): cancel() if err := <-completed; err != nil { t.Fatalf("process events: %v", err) } return messages, presenter.Entries() } } }