cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 364 lines 9.7 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strings" 8 "testing" 9 "time" 10 11 "github.com/stormlightlabs/noteleaf/internal/models" 12 "github.com/stormlightlabs/noteleaf/internal/repo" 13) 14 15type mockBookRepository struct { 16 books []*models.Book 17 err error 18} 19 20func (m *mockBookRepository) List(ctx context.Context, options repo.BookListOptions) ([]*models.Book, error) { 21 if m.err != nil { 22 return nil, m.err 23 } 24 25 var filtered []*models.Book 26 for _, book := range m.books { 27 if options.Status != "" && book.Status != options.Status { 28 continue 29 } 30 31 if options.Search != "" && !strings.Contains(strings.ToLower(book.Title), strings.ToLower(options.Search)) { 32 continue 33 } 34 35 filtered = append(filtered, book) 36 37 if options.Limit > 0 && len(filtered) >= options.Limit { 38 break 39 } 40 } 41 42 return filtered, nil 43} 44 45func TestBookAdapter(t *testing.T) { 46 now := time.Now() 47 book := &models.Book{ 48 ID: 1, 49 Title: "Test Book", 50 Author: "Test Author", 51 Status: "reading", 52 Progress: 45, 53 Pages: 250, 54 Rating: 4.5, 55 Notes: "Great book so far", 56 Added: now, 57 Started: &now, 58 } 59 60 t.Run("BookRecord", func(t *testing.T) { 61 record := &BookRecord{Book: book} 62 63 t.Run("GetField", func(t *testing.T) { 64 tests := []struct { 65 field string 66 expected any 67 name string 68 }{ 69 {"id", int64(1), "should return book ID"}, 70 {"title", "Test Book", "should return book title"}, 71 {"author", "Test Author", "should return book author"}, 72 {"status", "reading", "should return book status"}, 73 {"progress", 45, "should return book progress"}, 74 {"pages", 250, "should return page count"}, 75 {"rating", 4.5, "should return rating"}, 76 {"notes", "Great book so far", "should return notes"}, 77 {"added", now, "should return added time"}, 78 {"started", &now, "should return started time"}, 79 {"unknown", "", "should return empty string for unknown field"}, 80 } 81 82 for _, tt := range tests { 83 t.Run(tt.name, func(t *testing.T) { 84 result := record.GetField(tt.field) 85 86 switch expected := tt.expected.(type) { 87 case time.Time: 88 if resultTime, ok := result.(time.Time); !ok || !resultTime.Equal(expected) { 89 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 90 } 91 case *time.Time: 92 resultPtr, ok := result.(*time.Time) 93 if !ok || (expected == nil && resultPtr != nil) || (expected != nil && (resultPtr == nil || !resultPtr.Equal(*expected))) { 94 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 95 } 96 default: 97 if result != tt.expected { 98 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 99 } 100 } 101 }) 102 } 103 }) 104 105 t.Run("ListItem methods", func(t *testing.T) { 106 if record.GetTitle() != "Test Book" { 107 t.Errorf("GetTitle() = %q, want 'Test Book'", record.GetTitle()) 108 } 109 110 description := record.GetDescription() 111 if !strings.Contains(description, "Test Author") { 112 t.Errorf("GetDescription() should contain author, got: %s", description) 113 } 114 if !strings.Contains(description, "Reading") { 115 t.Errorf("GetDescription() should contain status, got: %s", description) 116 } 117 if !strings.Contains(description, "250 pages") { 118 t.Errorf("GetDescription() should contain page count, got: %s", description) 119 } 120 if !strings.Contains(description, "45%") { 121 t.Errorf("GetDescription() should contain progress, got: %s", description) 122 } 123 124 filterValue := record.GetFilterValue() 125 if !strings.Contains(filterValue, "Test Book") || !strings.Contains(filterValue, "Test Author") { 126 t.Errorf("GetFilterValue() should contain title and author, got: %s", filterValue) 127 } 128 if !strings.Contains(filterValue, "Great book so far") { 129 t.Errorf("GetFilterValue() should contain notes, got: %s", filterValue) 130 } 131 }) 132 133 t.Run("Model interface", func(t *testing.T) { 134 if record.GetID() != 1 { 135 t.Errorf("GetID() = %d, want 1", record.GetID()) 136 } 137 138 if record.GetTableName() != "books" { 139 t.Errorf("GetTableName() = %q, want 'books'", record.GetTableName()) 140 } 141 }) 142 }) 143 144 t.Run("BookDataSource", func(t *testing.T) { 145 books := []*models.Book{ 146 { 147 ID: 1, 148 Title: "Go Programming", 149 Author: "John Doe", 150 Status: "reading", 151 }, 152 { 153 ID: 2, 154 Title: "Python Guide", 155 Author: "Jane Smith", 156 Status: "queued", 157 }, 158 { 159 ID: 3, 160 Title: "Advanced Go", 161 Author: "Bob Johnson", 162 Status: "finished", 163 }, 164 } 165 166 t.Run("Load", func(t *testing.T) { 167 repo := &mockBookRepository{books: books} 168 source := &BookDataSource{repo: repo} 169 170 items, err := source.Load(context.Background(), ListOptions{}) 171 if err != nil { 172 t.Fatalf("Load() failed: %v", err) 173 } 174 175 if len(items) != 3 { 176 t.Errorf("Load() returned %d items, want 3", len(items)) 177 } 178 179 if items[0].GetTitle() != "Go Programming" { 180 t.Errorf("First item title = %q, want 'Go Programming'", items[0].GetTitle()) 181 } 182 }) 183 184 t.Run("Load with status filter", func(t *testing.T) { 185 repo := &mockBookRepository{books: books} 186 source := &BookDataSource{repo: repo, status: "reading"} 187 188 items, err := source.Load(context.Background(), ListOptions{}) 189 if err != nil { 190 t.Fatalf("Load() failed: %v", err) 191 } 192 193 if len(items) != 1 { 194 t.Errorf("Load() with status filter returned %d items, want 1", len(items)) 195 } 196 if items[0].GetTitle() != "Go Programming" { 197 t.Errorf("Filtered item title = %q, want 'Go Programming'", items[0].GetTitle()) 198 } 199 }) 200 201 t.Run("Search", func(t *testing.T) { 202 repo := &mockBookRepository{books: books} 203 source := &BookDataSource{repo: repo} 204 205 items, err := source.Search(context.Background(), "Python", ListOptions{}) 206 if err != nil { 207 t.Fatalf("Search() failed: %v", err) 208 } 209 210 if len(items) != 1 { 211 t.Errorf("Search() returned %d items, want 1", len(items)) 212 } 213 if items[0].GetTitle() != "Python Guide" { 214 t.Errorf("Search result title = %q, want 'Python Guide'", items[0].GetTitle()) 215 } 216 }) 217 218 t.Run("Load error", func(t *testing.T) { 219 testErr := fmt.Errorf("test error") 220 repo := &mockBookRepository{err: testErr} 221 source := &BookDataSource{repo: repo} 222 223 if _, err := source.Load(context.Background(), ListOptions{}); err != testErr { 224 t.Errorf("Load() error = %v, want %v", err, testErr) 225 } 226 }) 227 228 t.Run("Count", func(t *testing.T) { 229 repo := &mockBookRepository{books: books} 230 source := &BookDataSource{repo: repo} 231 232 count, err := source.Count(context.Background(), ListOptions{}) 233 if err != nil { 234 t.Fatalf("Count() failed: %v", err) 235 } 236 237 if count != 3 { 238 t.Errorf("Count() = %d, want 3", count) 239 } 240 }) 241 242 t.Run("Count error", func(t *testing.T) { 243 testErr := fmt.Errorf("test error") 244 repo := &mockBookRepository{err: testErr} 245 source := &BookDataSource{repo: repo} 246 247 _, err := source.Count(context.Background(), ListOptions{}) 248 if err != testErr { 249 t.Errorf("Count() error = %v, want %v", err, testErr) 250 } 251 }) 252 }) 253 254 t.Run("NewBookDataList", func(t *testing.T) { 255 repo := &mockBookRepository{ 256 books: []*models.Book{ 257 { 258 ID: 1, 259 Title: "Test Book", 260 Author: "Test Author", 261 Status: "reading", 262 }, 263 }, 264 } 265 266 opts := DataListOptions{ 267 Output: &bytes.Buffer{}, 268 Input: strings.NewReader("q\n"), 269 Static: true, 270 } 271 272 list := NewBookDataList(repo, opts, "") 273 if list == nil { 274 t.Fatal("NewBookDataList() returned nil") 275 } 276 277 err := list.Browse(context.Background()) 278 if err != nil { 279 t.Errorf("Browse() failed: %v", err) 280 } 281 }) 282 283 t.Run("NewBookListFromList", func(t *testing.T) { 284 repo := &mockBookRepository{ 285 books: []*models.Book{ 286 { 287 ID: 1, 288 Title: "Test Book", 289 Author: "Test Author", 290 Status: "reading", 291 }, 292 }, 293 } 294 295 output := &bytes.Buffer{} 296 input := strings.NewReader("q\n") 297 298 list := NewBookListFromList(repo, output, input, true, "") 299 if list == nil { 300 t.Fatal("NewBookListFromList() returned nil") 301 } 302 303 err := list.Browse(context.Background()) 304 if err != nil { 305 t.Errorf("Browse() failed: %v", err) 306 } 307 308 outputStr := output.String() 309 if !strings.Contains(outputStr, "Books") { 310 t.Error("Output should contain 'Books' title") 311 } 312 if !strings.Contains(outputStr, "Test Book") { 313 t.Error("Output should contain book title") 314 } 315 }) 316 317 t.Run("Format Book for View", func(t *testing.T) { 318 now := time.Now() 319 started := now.Add(-time.Hour) 320 321 book := &models.Book{ 322 ID: 1, 323 Title: "Test Book Title", 324 Author: "Test Author", 325 Status: "reading", 326 Progress: 75, 327 Pages: 250, 328 Rating: 4.5, 329 Notes: "This is a great book with detailed explanations.", 330 Added: now, 331 Started: &started, 332 } 333 334 result := formatBookForView(book) 335 336 if !strings.Contains(result, "Test Book Title") { 337 t.Error("Formatted view should contain book title") 338 } 339 if !strings.Contains(result, "Test Author") { 340 t.Error("Formatted view should contain author") 341 } 342 if !strings.Contains(result, "Reading") { 343 t.Error("Formatted view should contain status") 344 } 345 if !strings.Contains(result, "75%") { 346 t.Error("Formatted view should contain progress") 347 } 348 if !strings.Contains(result, "250") { 349 t.Error("Formatted view should contain page count") 350 } 351 if !strings.Contains(result, "4.5/5") { 352 t.Error("Formatted view should contain rating") 353 } 354 if !strings.Contains(result, "Added:") { 355 t.Error("Formatted view should contain added date") 356 } 357 if !strings.Contains(result, "Started:") { 358 t.Error("Formatted view should contain started date") 359 } 360 if !strings.Contains(result, "great book with detailed") { 361 t.Error("Formatted view should contain notes") 362 } 363 }) 364}