cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

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}