cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
at main 385 lines 10 kB view raw
1package ui 2 3import ( 4 "bytes" 5 "context" 6 "fmt" 7 "slices" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/stormlightlabs/noteleaf/internal/models" 13 "github.com/stormlightlabs/noteleaf/internal/repo" 14 "github.com/stormlightlabs/noteleaf/internal/shared" 15) 16 17type mockNoteRepository struct { 18 notes []*models.Note 19 err error 20} 21 22func (m *mockNoteRepository) List(ctx context.Context, options repo.NoteListOptions) ([]*models.Note, error) { 23 if m.err != nil { 24 return nil, m.err 25 } 26 27 var filtered []*models.Note 28 for _, note := range m.notes { 29 if options.Archived != nil && note.Archived != *options.Archived { 30 continue 31 } 32 33 if len(options.Tags) > 0 { 34 hasTag := false 35 for _, filterTag := range options.Tags { 36 if slices.Contains(note.Tags, filterTag) { 37 hasTag = true 38 break 39 } 40 } 41 if !hasTag { 42 continue 43 } 44 } 45 46 if options.Content != "" && !strings.Contains(note.Content, options.Content) { 47 continue 48 } 49 50 filtered = append(filtered, note) 51 52 if options.Limit > 0 && len(filtered) >= options.Limit { 53 break 54 } 55 } 56 57 return filtered, nil 58} 59 60func (m *mockNoteRepository) ListPublished(ctx context.Context) ([]*models.Note, error) { 61 if m.err != nil { 62 return nil, m.err 63 } 64 var published []*models.Note 65 for _, note := range m.notes { 66 if note.LeafletRKey != nil && !note.IsDraft { 67 published = append(published, note) 68 } 69 } 70 return published, nil 71} 72 73func (m *mockNoteRepository) ListDrafts(ctx context.Context) ([]*models.Note, error) { 74 if m.err != nil { 75 return nil, m.err 76 } 77 var drafts []*models.Note 78 for _, note := range m.notes { 79 if note.LeafletRKey != nil && note.IsDraft { 80 drafts = append(drafts, note) 81 } 82 } 83 return drafts, nil 84} 85 86func (m *mockNoteRepository) GetLeafletNotes(ctx context.Context) ([]*models.Note, error) { 87 if m.err != nil { 88 return nil, m.err 89 } 90 var leafletNotes []*models.Note 91 for _, note := range m.notes { 92 if note.LeafletRKey != nil { 93 leafletNotes = append(leafletNotes, note) 94 } 95 } 96 return leafletNotes, nil 97} 98 99func TestNoteAdapter(t *testing.T) { 100 t.Run("NoteRecord", func(t *testing.T) { 101 note := &models.Note{ 102 ID: 1, 103 Title: "Test Note", 104 Content: "This is test content", 105 Tags: []string{"work", "important"}, 106 Archived: false, 107 Created: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), 108 Modified: time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC), 109 FilePath: "/path/to/note.md", 110 } 111 record := &NoteRecord{Note: note} 112 113 t.Run("GetField", func(t *testing.T) { 114 tests := []struct { 115 field string 116 expected any 117 name string 118 }{ 119 {"id", int64(1), "should return note ID"}, 120 {"title", "Test Note", "should return note title"}, 121 {"content", "This is test content", "should return note content"}, 122 {"tags", []string{"work", "important"}, "should return note tags"}, 123 {"archived", false, "should return archived status"}, 124 {"unknown", "", "should return empty string for unknown field"}, 125 } 126 127 for _, tt := range tests { 128 t.Run(tt.name, func(t *testing.T) { 129 result := record.GetField(tt.field) 130 if tags, ok := tt.expected.([]string); ok { 131 resultTags, ok := result.([]string) 132 if !ok || len(resultTags) != len(tags) { 133 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 134 return 135 } 136 for i, tag := range tags { 137 if resultTags[i] != tag { 138 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 139 return 140 } 141 } 142 } else if result != tt.expected { 143 t.Errorf("GetField(%q) = %v, want %v", tt.field, result, tt.expected) 144 } 145 }) 146 } 147 }) 148 149 t.Run("ListItem methods", func(t *testing.T) { 150 if record.GetTitle() != "Test Note" { 151 t.Errorf("GetTitle() = %q, want 'Test Note'", record.GetTitle()) 152 } 153 154 description := record.GetDescription() 155 if !strings.Contains(description, "work, important") { 156 t.Errorf("GetDescription() should contain tags, got: %s", description) 157 } 158 if !strings.Contains(description, "2023-01-02 12:00") { 159 t.Errorf("GetDescription() should contain modified time, got: %s", description) 160 } 161 162 filterValue := record.GetFilterValue() 163 if !strings.Contains(filterValue, "Test Note") || !strings.Contains(filterValue, "work") { 164 t.Errorf("GetFilterValue() should contain title and tags, got: %s", filterValue) 165 } 166 }) 167 168 t.Run("Model interface", func(t *testing.T) { 169 if record.GetID() != 1 { 170 t.Errorf("GetID() = %d, want 1", record.GetID()) 171 } 172 173 if record.GetTableName() != "notes" { 174 t.Errorf("GetTableName() = %q, want 'notes'", record.GetTableName()) 175 } 176 }) 177 }) 178 179 t.Run("NoteDataSource", func(t *testing.T) { 180 notes := []*models.Note{ 181 { 182 ID: 1, 183 Title: "Work Note", 184 Content: "Work content", 185 Tags: []string{"work"}, 186 Archived: false, 187 Created: time.Now(), 188 Modified: time.Now(), 189 }, 190 { 191 ID: 2, 192 Title: "Personal Note", 193 Content: "Personal content", 194 Tags: []string{"personal"}, 195 Archived: false, 196 Created: time.Now(), 197 Modified: time.Now(), 198 }, 199 { 200 ID: 3, 201 Title: "Archived Note", 202 Content: "Archived content", 203 Tags: []string{"old"}, 204 Archived: true, 205 Created: time.Now(), 206 Modified: time.Now(), 207 }, 208 } 209 210 t.Run("Load", func(t *testing.T) { 211 repo := &mockNoteRepository{notes: notes} 212 source := &NoteDataSource{repo: repo, showArchived: true} 213 214 items, err := source.Load(context.Background(), ListOptions{}) 215 if err != nil { 216 t.Fatalf("Load() failed: %v", err) 217 } 218 219 if len(items) != 3 { 220 t.Errorf("Load() returned %d items, want 3", len(items)) 221 } 222 223 if items[0].GetTitle() != "Work Note" { 224 t.Errorf("First item title = %q, want 'Work Note'", items[0].GetTitle()) 225 } 226 }) 227 228 t.Run("Load with archived filter", func(t *testing.T) { 229 repo := &mockNoteRepository{notes: notes} 230 source := &NoteDataSource{repo: repo, showArchived: false} 231 232 items, err := source.Load(context.Background(), ListOptions{}) 233 if err != nil { 234 t.Fatalf("Load() failed: %v", err) 235 } 236 237 if len(items) != 2 { 238 t.Errorf("Load() with archived=false returned %d items, want 2", len(items)) 239 } 240 }) 241 242 t.Run("Load with tag filter", func(t *testing.T) { 243 repo := &mockNoteRepository{notes: notes} 244 source := &NoteDataSource{repo: repo, showArchived: true, tags: []string{"work"}} 245 246 items, err := source.Load(context.Background(), ListOptions{}) 247 if err != nil { 248 t.Fatalf("Load() failed: %v", err) 249 } 250 251 if len(items) != 1 { 252 t.Errorf("Load() with tags filter returned %d items, want 1", len(items)) 253 } 254 if items[0].GetTitle() != "Work Note" { 255 t.Errorf("Filtered item title = %q, want 'Work Note'", items[0].GetTitle()) 256 } 257 }) 258 259 t.Run("Search", func(t *testing.T) { 260 repo := &mockNoteRepository{notes: notes} 261 source := &NoteDataSource{repo: repo, showArchived: true} 262 263 items, err := source.Search(context.Background(), "Work", ListOptions{}) 264 if err != nil { 265 t.Fatalf("Search() failed: %v", err) 266 } 267 268 if len(items) != 1 { 269 t.Errorf("Search() returned %d items, want 1", len(items)) 270 } 271 if items[0].GetTitle() != "Work Note" { 272 t.Errorf("Search result title = %q, want 'Work Note'", items[0].GetTitle()) 273 } 274 }) 275 276 t.Run("Load error", func(t *testing.T) { 277 testErr := fmt.Errorf("test error") 278 repo := &mockNoteRepository{err: testErr} 279 source := &NoteDataSource{repo: repo} 280 281 _, err := source.Load(context.Background(), ListOptions{}) 282 if err != testErr { 283 t.Errorf("Load() error = %v, want %v", err, testErr) 284 } 285 }) 286 287 t.Run("Count", func(t *testing.T) { 288 repo := &mockNoteRepository{notes: notes} 289 source := &NoteDataSource{repo: repo, showArchived: true} 290 291 count, err := source.Count(context.Background(), ListOptions{}) 292 if err != nil { 293 t.Fatalf("Count() failed: %v", err) 294 } 295 296 if count != 3 { 297 t.Errorf("Count() = %d, want 3", count) 298 } 299 }) 300 }) 301 302 t.Run("NewNoteDataList", func(t *testing.T) { 303 repo := &mockNoteRepository{ 304 notes: []*models.Note{ 305 { 306 ID: 1, 307 Title: "Test Note", 308 Content: "Test content", 309 Tags: []string{"test"}, 310 Archived: false, 311 Created: time.Now(), 312 Modified: time.Now(), 313 }, 314 }, 315 } 316 317 opts := DataListOptions{ 318 Output: &bytes.Buffer{}, 319 Input: strings.NewReader("q\n"), 320 Static: true, 321 } 322 323 list := NewNoteDataList(repo, opts, false, nil) 324 if list == nil { 325 t.Fatal("NewNoteDataList() returned nil") 326 } 327 328 err := list.Browse(context.Background()) 329 if err != nil { 330 t.Errorf("Browse() failed: %v", err) 331 } 332 }) 333 334 t.Run("NewNoteListFromList", func(t *testing.T) { 335 repo := &mockNoteRepository{ 336 notes: []*models.Note{ 337 { 338 ID: 1, 339 Title: "Test Note", 340 Content: "Test content", 341 Tags: []string{"test"}, 342 Archived: false, 343 Created: time.Now(), 344 Modified: time.Now(), 345 }, 346 }, 347 } 348 349 output := &bytes.Buffer{} 350 input := strings.NewReader("q\n") 351 352 list := NewNoteListFromList(repo, output, input, true, false, nil) 353 if list == nil { 354 t.Fatal("NewNoteListFromList() returned nil") 355 } 356 357 err := list.Browse(context.Background()) 358 if err != nil { 359 t.Errorf("Browse() failed: %v", err) 360 } 361 362 outputStr := output.String() 363 shared.AssertContains(t, outputStr, "Notes", "Output should contain 'Notes' title") 364 shared.AssertContains(t, outputStr, "Test Note", "Output should contain note title") 365 }) 366 367 t.Run("Format Note for View", func(t *testing.T) { 368 note := &models.Note{ 369 ID: 1, 370 Title: "Test Note", 371 Content: "# Test Note\n\nThis is the content.", 372 Tags: []string{"test", "example"}, 373 Archived: false, 374 Created: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), 375 Modified: time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC), 376 } 377 378 result := formatNoteForView(note) 379 380 shared.AssertContains(t, result, "Test Note", "Formatted view should contain note title") 381 shared.AssertContains(t, result, "test", "Formatted view should contain tags") 382 shared.AssertContains(t, result, "2023-01-01", "Formatted view should contain created date") 383 shared.AssertContains(t, result, "2023-01-02", "Formatted view should contain modified date") 384 }) 385}