1package usecase_test
2
3import (
4 "context"
5 "math"
6 "testing"
7
8 "github.com/bnema/dumber/internal/application/dto"
9 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
10 "github.com/bnema/dumber/internal/application/usecase"
11 "github.com/stretchr/testify/mock"
12 "github.com/stretchr/testify/require"
13)
14
15func TestSaveWebUIConfigUseCase_Validation(t *testing.T) {
16 tests := []struct {
17 name string
18 mutate func(*dto.WebUIConfig)
19 wantErr string
20 }{
21 {
22 name: "rejects invalid default search URL",
23 mutate: func(cfg *dto.WebUIConfig) {
24 cfg.DefaultSearchEngine = "not-a-url-%s"
25 },
26 wantErr: "default_search_engine",
27 },
28 {
29 name: "rejects invalid performance profile",
30 mutate: func(cfg *dto.WebUIConfig) {
31 cfg.Performance.Profile = "turbo"
32 },
33 wantErr: "performance.profile",
34 },
35 {
36 name: "rejects NaN UI scale",
37 mutate: func(cfg *dto.WebUIConfig) {
38 cfg.DefaultUIScale = math.NaN()
39 },
40 wantErr: "default_ui_scale must be a finite value",
41 },
42 {
43 name: "rejects infinite UI scale",
44 mutate: func(cfg *dto.WebUIConfig) {
45 cfg.DefaultUIScale = math.Inf(1)
46 },
47 wantErr: "default_ui_scale must be a finite value",
48 },
49 }
50
51 for _, tt := range tests {
52 t.Run(tt.name, func(t *testing.T) {
53 cfg := validWebUIConfig()
54 tt.mutate(&cfg)
55 saver := portmocks.NewMockWebUIConfigSaver(t)
56 uc := usecase.NewSaveWebUIConfigUseCase(saver)
57
58 err := uc.Execute(context.Background(), cfg)
59
60 require.Error(t, err)
61 require.Contains(t, err.Error(), tt.wantErr)
62 })
63 }
64}
65
66func TestSaveWebUIConfigUseCase_RejectsDuplicateTrimmedSearchShortcutKeys(t *testing.T) {
67 saver := portmocks.NewMockWebUIConfigSaver(t)
68 uc := usecase.NewSaveWebUIConfigUseCase(saver)
69 cfg := validWebUIConfig()
70 cfg.SearchShortcuts["ddg"] = dto.SearchShortcut{URL: "https://duckduckgo.com/?q=%s", Description: "DuckDuckGo"}
71 cfg.SearchShortcuts[" ddg "] = dto.SearchShortcut{URL: "https://example.com/?q=%s", Description: "Duplicate"}
72
73 err := uc.Execute(context.Background(), cfg)
74
75 require.Error(t, err)
76 require.Contains(t, err.Error(), "duplicates")
77}
78
79func TestSaveWebUIConfigUseCase_NormalizesAndSavesValidConfig(t *testing.T) {
80 saver := portmocks.NewMockWebUIConfigSaver(t)
81 var saved dto.WebUIConfig
82 saver.EXPECT().SaveWebUIConfig(mock.Anything, mock.AnythingOfType("dto.WebUIConfig")).Run(func(_ context.Context, cfg dto.WebUIConfig) {
83 saved = cfg
84 }).Return(nil).Once()
85 uc := usecase.NewSaveWebUIConfigUseCase(saver)
86 cfg := validWebUIConfig()
87 cfg.SearchShortcuts[" ddg "] = dto.SearchShortcut{URL: " https://duckduckgo.com/?q=%s ", Description: " DuckDuckGo "}
88 cfg.SearchShortcuts[" "] = dto.SearchShortcut{URL: "https://example.com?q=%s", Description: "Example"}
89
90 err := uc.Execute(context.Background(), cfg)
91
92 require.NoError(t, err)
93 require.NotContains(t, saved.SearchShortcuts, " ddg ")
94 require.NotContains(t, saved.SearchShortcuts, "")
95 require.Contains(t, saved.SearchShortcuts, "ddg")
96 require.Equal(t, "https://duckduckgo.com/?q=%s", saved.SearchShortcuts["ddg"].URL)
97 require.Equal(t, "DuckDuckGo", saved.SearchShortcuts["ddg"].Description)
98}
99
100func TestSaveWebUIConfigUseCase_ClampsCustomPerformanceOnlyForCustomProfile(t *testing.T) {
101 out := dto.WebUICustomPerformanceConfig{
102 SkiaCPUThreads: 99,
103 SkiaGPUThreads: -5,
104 WebProcessMemoryMB: -1,
105 NetworkProcessMemoryMB: 99999,
106 WebViewPoolPrewarm: 500,
107 }
108
109 tests := []struct {
110 name string
111 profile string
112 want dto.WebUICustomPerformanceConfig
113 }{
114 {
115 name: "custom is clamped",
116 profile: " custom ",
117 want: dto.WebUICustomPerformanceConfig{
118 SkiaCPUThreads: 8,
119 SkiaGPUThreads: -1,
120 WebProcessMemoryMB: 0,
121 NetworkProcessMemoryMB: 4096,
122 WebViewPoolPrewarm: 20,
123 },
124 },
125 {name: "other profiles are untouched", profile: "balanced", want: out},
126 }
127
128 for _, tt := range tests {
129 t.Run(tt.name, func(t *testing.T) {
130 saver := portmocks.NewMockWebUIConfigSaver(t)
131 var saved dto.WebUIConfig
132 saver.EXPECT().SaveWebUIConfig(mock.Anything, mock.AnythingOfType("dto.WebUIConfig")).Run(func(_ context.Context, cfg dto.WebUIConfig) {
133 saved = cfg
134 }).Return(nil).Once()
135 cfg := validWebUIConfig()
136 cfg.Performance.Profile = tt.profile
137 cfg.Performance.Custom = out
138
139 require.NoError(t, usecase.NewSaveWebUIConfigUseCase(saver).Execute(context.Background(), cfg))
140 require.Equal(t, tt.want, saved.Performance.Custom)
141 })
142 }
143}
144
145func validWebUIConfig() dto.WebUIConfig {
146 palette := dto.ColorPalette{
147 Background: "#ffffff",
148 Surface: "#f8f8f8",
149 SurfaceVariant: "#eeeeee",
150 Text: "#111111",
151 Muted: "#666666",
152 Accent: "#0055ff",
153 Border: "#dddddd",
154 }
155 return dto.WebUIConfig{
156 Appearance: dto.WebUIAppearanceConfig{
157 SansFont: "Inter",
158 SerifFont: "Georgia",
159 MonospaceFont: "JetBrains Mono",
160 GtkFont: "Adwaita Sans",
161 DefaultFontSize: 16,
162 ColorScheme: "prefer-dark",
163 LightPalette: palette,
164 DarkPalette: palette,
165 },
166 Performance: dto.WebUIPerformanceConfig{
167 Profile: "default",
168 },
169 DefaultUIScale: 1,
170 DefaultSearchEngine: "https://duckduckgo.com/?q=%s",
171 SearchShortcuts: map[string]dto.SearchShortcut{},
172 }
173}