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

build: add interactive element tests

+194 -2
+89
internal/handlers/notes_test.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "fmt" 5 6 "os" 6 7 "path/filepath" 7 8 "runtime" ··· 381 382 t.Errorf("Expected empty editor, got %q", editor) 382 383 } 383 384 }) 385 + }) 386 + }) 387 + 388 + t.Run("CreateInteractive", func(t *testing.T) { 389 + ctx := context.Background() 390 + 391 + t.Run("creates note successfully", func(t *testing.T) { 392 + mockEditor := func(editor, filePath string) error { 393 + content := `# Test Interactive Note 394 + 395 + This is content from the interactive editor. 396 + 397 + <!-- Tags: interactive, test -->` 398 + return os.WriteFile(filePath, []byte(content), 0644) 399 + } 400 + 401 + handler.openInEditorFunc = mockEditor 402 + 403 + err := handler.createInteractive(ctx) 404 + if err != nil { 405 + t.Errorf("createInteractive should succeed: %v", err) 406 + } 407 + }) 408 + 409 + t.Run("handles cancelled note creation", func(t *testing.T) { 410 + mockEditor := func(editor, filePath string) error { 411 + return nil 412 + } 413 + 414 + handler.openInEditorFunc = mockEditor 415 + 416 + err := handler.createInteractive(ctx) 417 + if err != nil { 418 + t.Errorf("createInteractive should succeed even when cancelled: %v", err) 419 + } 420 + }) 421 + 422 + t.Run("handles editor error", func(t *testing.T) { 423 + mockEditor := func(editor, filePath string) error { 424 + return fmt.Errorf("editor failed to open") 425 + } 426 + 427 + handler.openInEditorFunc = mockEditor 428 + 429 + err := handler.createInteractive(ctx) 430 + if err == nil { 431 + t.Error("createInteractive should fail when editor fails") 432 + } 433 + if !strings.Contains(err.Error(), "failed to open editor") { 434 + t.Errorf("Expected editor error, got: %v", err) 435 + } 436 + }) 437 + 438 + t.Run("handles no editor configured", func(t *testing.T) { 439 + oldEditor := os.Getenv("EDITOR") 440 + oldPath := os.Getenv("PATH") 441 + os.Unsetenv("EDITOR") 442 + os.Setenv("PATH", "") 443 + defer func() { 444 + if oldEditor != "" { 445 + os.Setenv("EDITOR", oldEditor) 446 + } 447 + os.Setenv("PATH", oldPath) 448 + }() 449 + 450 + err := handler.createInteractive(ctx) 451 + if err == nil { 452 + t.Error("createInteractive should fail when no editor is configured") 453 + } 454 + if !strings.Contains(err.Error(), "no editor configured") { 455 + t.Errorf("Expected no editor error, got: %v", err) 456 + } 457 + }) 458 + 459 + t.Run("handles file read error after editing", func(t *testing.T) { 460 + mockEditor := func(editor, filePath string) error { 461 + return os.Remove(filePath) 462 + } 463 + 464 + handler.openInEditorFunc = mockEditor 465 + 466 + err := handler.createInteractive(ctx) 467 + if err == nil { 468 + t.Error("createInteractive should fail when temp file is deleted") 469 + } 470 + if !strings.Contains(err.Error(), "failed to read edited content") { 471 + t.Errorf("Expected read error, got: %v", err) 472 + } 384 473 }) 385 474 }) 386 475 }
+103
internal/handlers/tasks_test.go
··· 1 1 package handlers 2 2 3 3 import ( 4 + "bytes" 4 5 "context" 5 6 "os" 6 7 "runtime" ··· 12 13 13 14 "github.com/google/uuid" 14 15 "github.com/stormlightlabs/noteleaf/internal/models" 16 + "github.com/stormlightlabs/noteleaf/internal/ui" 15 17 ) 16 18 17 19 func setupTaskTest(t *testing.T) (string, func()) { ··· 922 924 if result != "s" { 923 925 t.Errorf("Expected 's' for 10, got '%s'", result) 924 926 } 927 + }) 928 + }) 929 + 930 + t.Run("InteractiveComponentsStatic", func(t *testing.T) { 931 + _, cleanup := setupTaskTest(t) 932 + defer cleanup() 933 + 934 + handler, err := NewTaskHandler() 935 + if err != nil { 936 + t.Fatalf("Failed to create task handler: %v", err) 937 + } 938 + defer handler.Close() 939 + 940 + ctx := context.Background() 941 + 942 + err = handler.Create(ctx, []string{"Test", "Task", "1"}, "high", "test-project", "test-context", "", []string{"tag1"}) 943 + if err != nil { 944 + t.Fatalf("Failed to create test task: %v", err) 945 + } 946 + 947 + err = handler.Create(ctx, []string{"Test", "Task", "2"}, "medium", "test-project", "test-context", "", []string{"tag2"}) 948 + if err != nil { 949 + t.Fatalf("Failed to create test task: %v", err) 950 + } 951 + 952 + t.Run("taskListStaticMode", func(t *testing.T) { 953 + var output bytes.Buffer 954 + 955 + t.Run("lists all tasks", func(t *testing.T) { 956 + output.Reset() 957 + taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, true, "", "", "") 958 + err := taskTable.Browse(ctx) 959 + if err != nil { 960 + t.Errorf("Static task list should succeed: %v", err) 961 + } 962 + if !strings.Contains(output.String(), "Test Task 1") { 963 + t.Error("Output should contain Test Task 1") 964 + } 965 + if !strings.Contains(output.String(), "Test Task 2") { 966 + t.Error("Output should contain Test Task 2") 967 + } 968 + }) 969 + 970 + t.Run("filters by status", func(t *testing.T) { 971 + output.Reset() 972 + taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "pending", "", "") 973 + err := taskTable.Browse(ctx) 974 + if err != nil { 975 + t.Errorf("Static task list with status filter should succeed: %v", err) 976 + } 977 + }) 978 + 979 + t.Run("filters by priority", func(t *testing.T) { 980 + output.Reset() 981 + taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "high", "") 982 + err := taskTable.Browse(ctx) 983 + if err != nil { 984 + t.Errorf("Static task list with priority filter should succeed: %v", err) 985 + } 986 + }) 987 + 988 + t.Run("filters by project", func(t *testing.T) { 989 + output.Reset() 990 + taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "", "test-project") 991 + err := taskTable.Browse(ctx) 992 + if err != nil { 993 + t.Errorf("Static task list with project filter should succeed: %v", err) 994 + } 995 + }) 996 + }) 997 + 998 + t.Run("projectListStaticMode", func(t *testing.T) { 999 + var output bytes.Buffer 1000 + 1001 + t.Run("lists projects", func(t *testing.T) { 1002 + output.Reset() 1003 + projectTable := ui.NewProjectListFromTable(handler.repos.Tasks, &output, os.Stdin, true) 1004 + err := projectTable.Browse(ctx) 1005 + if err != nil { 1006 + t.Errorf("Static project list should succeed: %v", err) 1007 + } 1008 + if !strings.Contains(output.String(), "test-project") { 1009 + t.Error("Output should contain test-project") 1010 + } 1011 + }) 1012 + }) 1013 + 1014 + t.Run("tagListStaticMode", func(t *testing.T) { 1015 + var output bytes.Buffer 1016 + 1017 + t.Run("lists tags", func(t *testing.T) { 1018 + output.Reset() 1019 + tagTable := ui.NewTagListFromTable(handler.repos.Tasks, &output, os.Stdin, true) 1020 + err := tagTable.Browse(ctx) 1021 + if err != nil { 1022 + t.Errorf("Static tag list should succeed: %v", err) 1023 + } 1024 + if !strings.Contains(output.String(), "tag1") { 1025 + t.Error("Output should contain tag1") 1026 + } 1027 + }) 925 1028 }) 926 1029 }) 927 1030 }
+2 -2
justfile
··· 15 15 @echo "Coverage report generated: coverage.html" 16 16 17 17 # Run tests and show coverage in terminal 18 - test-coverage: 18 + cov: 19 19 go test ./... -coverprofile=coverage.out 20 20 go tool cover -func=coverage.out 21 21 ··· 36 36 go fmt ./... 37 37 38 38 # Run all quality checks 39 - check: lint test-coverage 39 + check: lint cov 40 40 41 41 # Install dependencies 42 42 deps: