1
0

feat: 增强桌面启动失败提示与测试覆盖

This commit is contained in:
2026-05-08 23:42:48 +08:00
parent c524e8f928
commit 2dec9e5c54
21 changed files with 1857 additions and 297 deletions

View File

@@ -0,0 +1,169 @@
package main
import (
"errors"
"sync"
"testing"
"time"
"go.uber.org/zap"
)
type fakeTrayController struct {
run func(onReady func(), onExit func())
quitCh chan struct{}
quitOnce sync.Once
icon []byte
tooltip string
menuItems []*fakeTrayMenuItem
}
func newFakeTrayController() *fakeTrayController {
return &fakeTrayController{quitCh: make(chan struct{})}
}
func (c *fakeTrayController) Run(onReady func(), onExit func()) {
if c.run != nil {
c.run(onReady, onExit)
return
}
onReady()
<-c.quitCh
if onExit != nil {
onExit()
}
}
func (c *fakeTrayController) Quit() {
c.quitOnce.Do(func() { close(c.quitCh) })
}
func (c *fakeTrayController) SetIcon(icon []byte) {
c.icon = append([]byte(nil), icon...)
}
func (c *fakeTrayController) SetTooltip(tooltip string) {
c.tooltip = tooltip
}
func (c *fakeTrayController) AddMenuItem(title, tooltip string) trayMenuItem {
item := &fakeTrayMenuItem{clicked: make(chan struct{}), title: title, tooltip: tooltip}
c.menuItems = append(c.menuItems, item)
return item
}
func (c *fakeTrayController) AddSeparator() {}
type fakeTrayMenuItem struct {
clicked chan struct{}
title string
tooltip string
disabled bool
}
func (m *fakeTrayMenuItem) Disable() {
m.disabled = true
}
func (m *fakeTrayMenuItem) Clicked() <-chan struct{} {
return m.clicked
}
func TestRunSystrayReadyOpensBrowser(t *testing.T) {
controller := newFakeTrayController()
opened := make(chan string, 1)
err := runSystray(19826, trayOptions{
controller: controller,
readyTimeout: time.Second,
iconLoader: func() ([]byte, error) { return []byte("icon"), nil },
openBrowser: func(url string) error {
opened <- url
controller.Quit()
return nil
},
notify: func(string, string) {},
logger: zap.NewNop(),
})
if err != nil {
t.Fatalf("托盘 ready 成功不应返回错误: %v", err)
}
if got := <-opened; got != "http://localhost:19826" {
t.Fatalf("浏览器 URL = %s", got)
}
if string(controller.icon) != "icon" {
t.Fatalf("应设置托盘图标")
}
if controller.tooltip != appTooltip {
t.Fatalf("tooltip = %q, want %q", controller.tooltip, appTooltip)
}
}
func TestRunSystrayReadyTimeoutReturnsTrayStartupError(t *testing.T) {
controller := newFakeTrayController()
controller.run = func(_ func(), _ func()) {
<-controller.quitCh
}
err := runSystray(19826, trayOptions{
controller: controller,
readyTimeout: 10 * time.Millisecond,
iconLoader: func() ([]byte, error) { return []byte("icon"), nil },
openBrowser: func(string) error { return nil },
notify: func(string, string) {},
logger: zap.NewNop(),
})
if err == nil {
t.Fatal("托盘 ready timeout 应返回错误")
}
var startupErr *startupError
if !errors.As(err, &startupErr) || startupErr.Phase() != "tray" {
t.Fatalf("应返回 tray 阶段启动错误,实际: %v", err)
}
}
func TestRunSystrayIconLoadFailureReturnsTrayStartupError(t *testing.T) {
controller := newFakeTrayController()
err := runSystray(19826, trayOptions{
controller: controller,
readyTimeout: time.Second,
iconLoader: func() ([]byte, error) { return nil, errors.New("missing icon") },
openBrowser: func(string) error { return nil },
notify: func(string, string) {},
logger: zap.NewNop(),
})
if err == nil {
t.Fatal("托盘图标加载失败应返回错误")
}
var startupErr *startupError
if !errors.As(err, &startupErr) || startupErr.Phase() != "tray" {
t.Fatalf("应返回 tray 阶段启动错误,实际: %v", err)
}
}
func TestRunSystrayBrowserOpenFailureIsNonFatal(t *testing.T) {
controller := newFakeTrayController()
notified := make(chan string, 1)
err := runSystray(19826, trayOptions{
controller: controller,
readyTimeout: time.Second,
iconLoader: func() ([]byte, error) { return []byte("icon"), nil },
openBrowser: func(string) error { return errors.New("no browser") },
notify: func(_, message string) {
notified <- message
controller.Quit()
},
logger: zap.NewNop(),
})
if err != nil {
t.Fatalf("浏览器打开失败不应成为 fatal: %v", err)
}
if got := <-notified; got == "" {
t.Fatal("浏览器打开失败应提示用户")
}
}