tangled
alpha
login
or
join now
desertthunder.dev
/
noteleaf
cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐
charm
leaflet
readability
golang
29
fork
atom
overview
issues
2
pulls
pipelines
fix: note list testing
desertthunder.dev
5 months ago
7da45884
1b08b0ca
+292
-1
2 changed files
expand all
collapse all
unified
split
internal
ui
note_list.go
note_list_test.go
-1
internal/ui/note_list.go
···
1
-
// FIXME: this module is missing test coverage
2
package ui
3
4
import (
···
0
1
package ui
2
3
import (
+292
internal/ui/note_list_test.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}