cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
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}