cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

fix: note list testing

+292 -1
-1
internal/ui/note_list.go
··· 1 - // FIXME: this module is missing test coverage 2 1 package ui 3 2 4 3 import (
+292
internal/ui/note_list_test.go
··· 1 + package ui 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "database/sql" 7 + "errors" 8 + "strings" 9 + "testing" 10 + "time" 11 + 12 + _ "github.com/mattn/go-sqlite3" 13 + "github.com/stormlightlabs/noteleaf/internal/models" 14 + "github.com/stormlightlabs/noteleaf/internal/repo" 15 + ) 16 + 17 + // newTestNoteRepo sets up an in-memory SQLite database and returns a [repo.NoteRepository]. 18 + func newTestNoteRepo(t *testing.T) (*repo.NoteRepository, func()) { 19 + t.Helper() 20 + 21 + db, err := sql.Open("sqlite3", ":memory:") 22 + if err != nil { 23 + t.Fatalf("Failed to open in-memory database: %v", err) 24 + } 25 + 26 + // Schema based on [models.Note] and observed errors 27 + createTableSQL := ` 28 + CREATE TABLE notes ( 29 + id INTEGER PRIMARY KEY AUTOINCREMENT, 30 + title TEXT NOT NULL, 31 + content TEXT, 32 + tags TEXT, 33 + created DATETIME NOT NULL, 34 + modified DATETIME NOT NULL, 35 + archived BOOLEAN NOT NULL DEFAULT 0, 36 + file_path TEXT 37 + );` 38 + if _, err := db.Exec(createTableSQL); err != nil { 39 + t.Fatalf("Failed to create notes table: %v", err) 40 + } 41 + 42 + noteRepo := repo.NewNoteRepository(db) 43 + 44 + cleanup := func() { 45 + db.Close() 46 + } 47 + 48 + return noteRepo, cleanup 49 + } 50 + 51 + func TestNoteListOptions(t *testing.T) { 52 + t.Run("default options", func(t *testing.T) { 53 + repo, cleanup := newTestNoteRepo(t) 54 + defer cleanup() 55 + 56 + nl := NewNoteList(repo, NoteListOptions{}) 57 + if nl.opts.Static { 58 + t.Error("Static should default to false") 59 + } 60 + }) 61 + 62 + t.Run("custom options", func(t *testing.T) { 63 + repo, cleanup := newTestNoteRepo(t) 64 + defer cleanup() 65 + 66 + var buf bytes.Buffer 67 + var in strings.Reader 68 + 69 + opts := NoteListOptions{ 70 + Output: &buf, 71 + Input: &in, 72 + Static: true, 73 + } 74 + 75 + nl := NewNoteList(repo, opts) 76 + 77 + if !nl.opts.Static { 78 + t.Error("Static should be true") 79 + } 80 + if nl.opts.Output != &buf { 81 + t.Error("Output should be set to buffer") 82 + } 83 + if nl.opts.Input != &in { 84 + t.Error("Input should be set to reader") 85 + } 86 + }) 87 + } 88 + 89 + func TestStaticList(t *testing.T) { 90 + ctx := context.Background() 91 + 92 + t.Run("no notes", func(t *testing.T) { 93 + repo, cleanup := newTestNoteRepo(t) 94 + defer cleanup() 95 + 96 + var buf bytes.Buffer 97 + nl := NewNoteList(repo, NoteListOptions{ 98 + Output: &buf, 99 + Static: true, 100 + }) 101 + 102 + err := nl.Browse(ctx) 103 + if err != nil { 104 + t.Fatalf("Browse failed: %v", err) 105 + } 106 + 107 + output := buf.String() 108 + if !strings.Contains(output, "No notes found") { 109 + t.Errorf("Expected 'No notes found', got %q", output) 110 + } 111 + }) 112 + 113 + t.Run("with notes", func(t *testing.T) { 114 + repo, cleanup := newTestNoteRepo(t) 115 + defer cleanup() 116 + 117 + // Create some instances of [models.Note] 118 + note1 := &models.Note{Title: "Test Note 1", Content: "Content 1", Tags: []string{"t1"}, Created: time.Now(), Modified: time.Now()} 119 + note2 := &models.Note{Title: "Test Note 2", Content: "Content 2", Tags: []string{"t2"}, Created: time.Now(), Modified: time.Now()} 120 + if _, err := repo.Create(ctx, note1); err != nil { 121 + t.Fatalf("Failed to create note: %v", err) 122 + } 123 + if _, err := repo.Create(ctx, note2); err != nil { 124 + t.Fatalf("Failed to create note: %v", err) 125 + } 126 + 127 + var buf bytes.Buffer 128 + nl := NewNoteList(repo, NoteListOptions{ 129 + Output: &buf, 130 + Static: true, 131 + }) 132 + 133 + err := nl.Browse(ctx) 134 + if err != nil { 135 + t.Fatalf("Browse failed: %v", err) 136 + } 137 + 138 + output := buf.String() 139 + if !strings.Contains(output, "Test Note 1") { 140 + t.Error("Output does not contain first note") 141 + } 142 + if !strings.Contains(output, "Test Note 2") { 143 + t.Error("Output does not contain second note") 144 + } 145 + if !strings.Contains(output, "t1") { 146 + t.Error("Output does not contain first note's tag") 147 + } 148 + }) 149 + 150 + t.Run("with archived notes", func(t *testing.T) { 151 + repo, cleanup := newTestNoteRepo(t) 152 + defer cleanup() 153 + 154 + note1 := &models.Note{Title: "Active Note", Created: time.Now(), Modified: time.Now()} 155 + note2 := &models.Note{Title: "Archived Note", Archived: true, Created: time.Now(), Modified: time.Now()} 156 + if _, err := repo.Create(ctx, note1); err != nil { 157 + t.Fatalf("Failed to create note: %v", err) 158 + } 159 + if _, err := repo.Create(ctx, note2); err != nil { 160 + t.Fatalf("Failed to create note: %v", err) 161 + } 162 + 163 + var buf bytes.Buffer 164 + nl := NewNoteList(repo, NoteListOptions{ 165 + Output: &buf, 166 + Static: true, 167 + ShowArchived: false, 168 + }) 169 + 170 + // Test with ShowArchived: false (default behavior) 171 + err := nl.Browse(ctx) 172 + if err != nil { 173 + t.Fatalf("Browse failed: %v", err) 174 + } 175 + 176 + output := buf.String() 177 + if !strings.Contains(output, "Active Note") { 178 + t.Error("Expected to see active note") 179 + } 180 + if strings.Contains(output, "Archived Note") { 181 + t.Error("Did not expect to see archived note") 182 + } 183 + 184 + buf.Reset() 185 + nl.opts.ShowArchived = true 186 + err = nl.Browse(ctx) 187 + if err != nil { 188 + t.Fatalf("Browse failed: %v", err) 189 + } 190 + 191 + output = buf.String() 192 + if !strings.Contains(output, "Active Note") { 193 + t.Error("Expected to see active note") 194 + } 195 + if !strings.Contains(output, "Archived Note") { 196 + t.Error("Expected to see archived note") 197 + } 198 + }) 199 + } 200 + 201 + func TestNoteListModelView(t *testing.T) { 202 + repo, cleanup := newTestNoteRepo(t) 203 + defer cleanup() 204 + 205 + opts := NoteListOptions{} 206 + 207 + t.Run("initial view", func(t *testing.T) { 208 + model := noteListModel{ 209 + repo: repo, 210 + opts: opts, 211 + notes: []*models.Note{}, 212 + } 213 + view := model.View() 214 + if !strings.Contains(view, "No notes found") { 215 + t.Errorf("Expected 'No notes found', got %q", view) 216 + } 217 + }) 218 + 219 + t.Run("error view", func(t *testing.T) { 220 + model := noteListModel{ 221 + repo: repo, 222 + opts: opts, 223 + err: errors.New("test error"), 224 + } 225 + view := model.View() 226 + if !strings.Contains(view, "Error: test error") { 227 + t.Errorf("Expected error message, got %q", view) 228 + } 229 + }) 230 + 231 + t.Run("with notes view", func(t *testing.T) { 232 + note := &models.Note{ID: 1, Title: "My Test Note", Tags: []string{"testing"}, Modified: time.Now()} 233 + model := noteListModel{ 234 + repo: repo, 235 + opts: opts, 236 + notes: []*models.Note{note}, 237 + selected: 0, 238 + } 239 + view := model.View() 240 + if !strings.Contains(view, "My Test Note") { 241 + t.Error("Expected to see note title") 242 + } 243 + if !strings.Contains(view, ">") { 244 + t.Error("Expected to see selection indicator") 245 + } 246 + }) 247 + 248 + t.Run("viewing note", func(t *testing.T) { 249 + model := noteListModel{ 250 + repo: repo, 251 + opts: opts, 252 + viewing: true, 253 + viewContent: "## My Note Content", 254 + } 255 + view := model.View() 256 + if !strings.Contains(view, "## My Note Content") { 257 + t.Error("Expected to see note content") 258 + } 259 + if !strings.Contains(view, "Press q/esc/backspace to return to list") { 260 + t.Error("Expected to see exit instructions") 261 + } 262 + }) 263 + } 264 + 265 + func TestFormatNoteForView(t *testing.T) { 266 + note := &models.Note{ 267 + Title: "My Note Title", 268 + Content: "This is the content.", 269 + Tags: []string{"go", "testing"}, 270 + Created: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), 271 + Modified: time.Date(2023, 1, 1, 13, 0, 0, 0, time.UTC), 272 + } 273 + 274 + m := noteListModel{} 275 + formatted := m.formatNoteForView(note) 276 + 277 + if !strings.Contains(formatted, "# My Note Title") { 278 + t.Error("Expected title markdown") 279 + } 280 + if !strings.Contains(formatted, "**Tags:** `go`, `testing`") { 281 + t.Error("Expected tags markdown") 282 + } 283 + if !strings.Contains(formatted, "**Created:** 2023-01-01 12:00") { 284 + t.Error("Expected created timestamp") 285 + } 286 + if !strings.Contains(formatted, "**Modified:** 2023-01-01 13:00") { 287 + t.Error("Expected modified timestamp") 288 + } 289 + if !strings.Contains(formatted, "This is the content.") { 290 + t.Error("Expected content") 291 + } 292 + }