verstak-desktop/internal/shell/tray/controller_test.go

228 lines
6.1 KiB
Go

package tray
import (
"errors"
"sync"
"testing"
"time"
)
type fakeMenuItem struct {
clicked chan struct{}
title string
tooltip string
}
func (i *fakeMenuItem) Clicked() <-chan struct{} { return i.clicked }
func (i *fakeMenuItem) SetTitle(title string) { i.title = title }
func (i *fakeMenuItem) SetTooltip(tooltip string) { i.tooltip = tooltip }
type fakeBackend struct {
callbacks BackendCallbacks
icon []byte
tooltip string
items map[string]*fakeMenuItem
startErr error
iconErr error
menuErr error
starts int
stops int
}
func (b *fakeBackend) Start(callbacks BackendCallbacks) error {
b.starts++
b.callbacks = callbacks
return b.startErr
}
func (b *fakeBackend) SetIcon(icon []byte) error {
b.icon = append([]byte(nil), icon...)
return b.iconErr
}
func (b *fakeBackend) SetTooltip(tooltip string) error {
b.tooltip = tooltip
return nil
}
func (b *fakeBackend) AddMenuItem(title, tooltip string) (MenuItem, error) {
if b.menuErr != nil {
return nil, b.menuErr
}
item := &fakeMenuItem{clicked: make(chan struct{}, 1), title: title, tooltip: tooltip}
b.items[title] = item
return item, nil
}
func (b *fakeBackend) Stop() { b.stops++ }
func (b *fakeBackend) ready() {
if b.callbacks.Ready != nil {
b.callbacks.Ready()
}
}
func (b *fakeBackend) exited() {
if b.callbacks.Exit != nil {
b.callbacks.Exit()
}
}
func (b *fakeBackend) leftClick() {
if b.callbacks.LeftClick != nil {
b.callbacks.LeftClick()
}
}
func waitFor(t *testing.T, signal <-chan struct{}) {
t.Helper()
select {
case <-signal:
case <-time.After(time.Second):
t.Fatal("timed out waiting for tray action")
}
}
func TestControllerBecomesReadyOnlyAfterBackendCallback(t *testing.T) {
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
controller := New(backend, []byte{1, 2, 3})
if err := controller.Start(Actions{}); err != nil {
t.Fatalf("Start() error = %v", err)
}
if controller.Ready() {
t.Fatal("tray became ready before backend callback")
}
backend.ready()
if !controller.Ready() {
t.Fatal("tray did not become ready after backend callback")
}
if string(backend.icon) != string([]byte{1, 2, 3}) || backend.tooltip != "Verstak" {
t.Fatalf("tray initialization = icon:%v tooltip:%q", backend.icon, backend.tooltip)
}
}
func TestControllerRoutesLeftClickAndMenuActionsToShowAndQuit(t *testing.T) {
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
showCalls := make(chan struct{}, 2)
quitCalls := make(chan struct{}, 1)
controller := New(backend, []byte{1})
if err := controller.Start(Actions{
Show: func() { showCalls <- struct{}{} },
Quit: func() { quitCalls <- struct{}{} },
}); err != nil {
t.Fatalf("Start() error = %v", err)
}
backend.ready()
backend.leftClick()
waitFor(t, showCalls)
showItem := backend.items["Show Verstak"]
quitItem := backend.items["Quit"]
if showItem == nil || quitItem == nil {
t.Fatalf("tray menu = %#v, want Show Verstak and Quit", backend.items)
}
showItem.clicked <- struct{}{}
waitFor(t, showCalls)
quitItem.clicked <- struct{}{}
waitFor(t, quitCalls)
}
func TestControllerStopCallsBackendOnce(t *testing.T) {
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
controller := New(backend, []byte{1})
if err := controller.Start(Actions{}); err != nil {
t.Fatalf("Start() error = %v", err)
}
controller.Stop()
controller.Stop()
if backend.stops != 1 {
t.Fatalf("backend stop calls = %d, want 1", backend.stops)
}
}
func TestControllerStartupAndSetupFailuresNeverBecomeReady(t *testing.T) {
for name, backend := range map[string]*fakeBackend{
"start": {items: make(map[string]*fakeMenuItem), startErr: errors.New("start failed")},
"icon": {items: make(map[string]*fakeMenuItem), iconErr: errors.New("icon failed")},
"menu": {items: make(map[string]*fakeMenuItem), menuErr: errors.New("menu failed")},
} {
t.Run(name, func(t *testing.T) {
controller := New(backend, []byte{1})
err := controller.Start(Actions{})
if name == "start" && err == nil {
t.Fatal("Start() error = nil, want startup failure")
}
if name != "start" && err != nil {
t.Fatalf("Start() error = %v, want nil before ready callback", err)
}
backend.ready()
if controller.Ready() {
t.Fatal("failed tray became ready")
}
})
}
}
func TestControllerExitRevokesReadinessAndKeepsLocalizedMenuItems(t *testing.T) {
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
controller := New(backend, []byte{1})
controller.SetLabels(LabelsForPreference("ru"))
if err := controller.Start(Actions{}); err != nil {
t.Fatalf("Start() error = %v", err)
}
backend.ready()
show := backend.items["Показать Верстак"]
quit := backend.items["Выйти"]
if show == nil || quit == nil {
t.Fatalf("Russian tray menu = %#v", backend.items)
}
controller.SetLabels(LabelsForPreference("en"))
if show.title != "Show Verstak" || quit.title != "Quit" {
t.Fatalf("English tray menu after update = show:%q quit:%q", show.title, quit.title)
}
backend.exited()
if controller.Ready() {
t.Fatal("tray stayed ready after backend exit")
}
}
func TestLabelsForSystemRussianLocale(t *testing.T) {
labels := LabelsForPreference("system", "", "ru_RU.UTF-8", "en_US.UTF-8")
if labels.ShowTitle != "Показать Верстак" || labels.QuitTitle != "Выйти" {
t.Fatalf("system Russian labels = %#v", labels)
}
labels = LabelsForPreference("system", "C.UTF-8", "", "ru_RU.UTF-8")
if labels.ShowTitle != "Show Verstak" || labels.QuitTitle != "Quit" {
t.Fatalf("higher-priority system locale must win, got %#v", labels)
}
}
func TestControllerReadyNotificationIsSafeAcrossCallbacks(t *testing.T) {
backend := &fakeBackend{items: make(map[string]*fakeMenuItem)}
controller := New(backend, []byte{1})
var changes []bool
var mu sync.Mutex
controller.SetReadyChangedHandler(func(ready bool) {
mu.Lock()
changes = append(changes, ready)
mu.Unlock()
})
if err := controller.Start(Actions{}); err != nil {
t.Fatalf("Start() error = %v", err)
}
backend.ready()
backend.exited()
mu.Lock()
defer mu.Unlock()
if len(changes) != 2 || !changes[0] || changes[1] {
t.Fatalf("ready changes = %#v, want [true false]", changes)
}
}