Add Filter builder, Archive/Unarchive, WithRepeat, and GoDoc examples
- Implement client-side Filter builder with tag, status, due date, and search filters - Add unit tests for Filter with performance benchmark - Add Archive/Unarchive methods to ChecklistService - Add WithRepeat method to TaskBuilder for recurring tasks - Create GoDoc examples for all major functionality
This commit is contained in:
parent
45e6b6eb18
commit
cb30b178be
7 changed files with 793 additions and 10 deletions
338
filter_test.go
Normal file
338
filter_test.go
Normal file
|
|
@ -0,0 +1,338 @@
|
|||
package checkvist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFilter_WithTag(t *testing.T) {
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent"},
|
||||
{ID: 2, Content: "Task 2", TagsAsText: "urgent"},
|
||||
{ID: 3, Content: "Task 3", TagsAsText: ""},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
expected []int
|
||||
}{
|
||||
{"filter by important", "important", []int{1}},
|
||||
{"filter by urgent", "urgent", []int{1, 2}},
|
||||
{"filter by nonexistent", "nonexistent", []int{}},
|
||||
{"case insensitive", "IMPORTANT", []int{1}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NewFilter(tasks).WithTag(tt.tag).Apply()
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||
return
|
||||
}
|
||||
for i, task := range result {
|
||||
if task.ID != tt.expected[i] {
|
||||
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithMultipleTags(t *testing.T) {
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Task 1", TagsAsText: "important, urgent, work"},
|
||||
{ID: 2, Content: "Task 2", TagsAsText: "urgent, work"},
|
||||
{ID: 3, Content: "Task 3", TagsAsText: "important"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
tags []string
|
||||
expected []int
|
||||
}{
|
||||
{"single tag", []string{"urgent"}, []int{1, 2}},
|
||||
{"two tags AND", []string{"urgent", "work"}, []int{1, 2}},
|
||||
{"three tags AND", []string{"important", "urgent", "work"}, []int{1}},
|
||||
{"no match", []string{"important", "urgent", "missing"}, []int{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NewFilter(tasks).WithTags(tt.tags...).Apply()
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||
return
|
||||
}
|
||||
for i, task := range result {
|
||||
if task.ID != tt.expected[i] {
|
||||
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithStatus(t *testing.T) {
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Open task", Status: StatusOpen},
|
||||
{ID: 2, Content: "Closed task", Status: StatusClosed},
|
||||
{ID: 3, Content: "Invalidated task", Status: StatusInvalidated},
|
||||
{ID: 4, Content: "Another open", Status: StatusOpen},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status TaskStatus
|
||||
expected []int
|
||||
}{
|
||||
{"open tasks", StatusOpen, []int{1, 4}},
|
||||
{"closed tasks", StatusClosed, []int{2}},
|
||||
{"invalidated tasks", StatusInvalidated, []int{3}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NewFilter(tasks).WithStatus(tt.status).Apply()
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||
return
|
||||
}
|
||||
for i, task := range result {
|
||||
if task.ID != tt.expected[i] {
|
||||
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithDueBefore(t *testing.T) {
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
nextWeek := now.AddDate(0, 0, 7)
|
||||
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
|
||||
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
|
||||
{ID: 3, Content: "Next week", DueDate: &nextWeek},
|
||||
{ID: 4, Content: "No due date", DueDate: nil},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).WithDueBefore(now).Apply()
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1 task, got %d", len(result))
|
||||
return
|
||||
}
|
||||
if result[0].ID != 1 {
|
||||
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithDueAfter(t *testing.T) {
|
||||
now := time.Now()
|
||||
yesterday := now.AddDate(0, 0, -1)
|
||||
tomorrow := now.AddDate(0, 0, 1)
|
||||
nextWeek := now.AddDate(0, 0, 7)
|
||||
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Yesterday", DueDate: &yesterday},
|
||||
{ID: 2, Content: "Tomorrow", DueDate: &tomorrow},
|
||||
{ID: 3, Content: "Next week", DueDate: &nextWeek},
|
||||
{ID: 4, Content: "No due date", DueDate: nil},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).WithDueAfter(now).Apply()
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||
return
|
||||
}
|
||||
if result[0].ID != 2 || result[1].ID != 3 {
|
||||
t.Errorf("expected task IDs [2, 3], got [%d, %d]", result[0].ID, result[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithDueOn(t *testing.T) {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
todayMorning := today.Add(9 * time.Hour)
|
||||
todayEvening := today.Add(18 * time.Hour)
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Today morning", DueDate: &todayMorning},
|
||||
{ID: 2, Content: "Today evening", DueDate: &todayEvening},
|
||||
{ID: 3, Content: "Tomorrow", DueDate: &tomorrow},
|
||||
{ID: 4, Content: "No due date", DueDate: nil},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).WithDueOn(today).Apply()
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||
return
|
||||
}
|
||||
if result[0].ID != 1 || result[1].ID != 2 {
|
||||
t.Errorf("expected task IDs [1, 2], got [%d, %d]", result[0].ID, result[1].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithOverdue(t *testing.T) {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
yesterday := today.AddDate(0, 0, -1)
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Overdue open", DueDate: &yesterday, Status: StatusOpen},
|
||||
{ID: 2, Content: "Overdue closed", DueDate: &yesterday, Status: StatusClosed},
|
||||
{ID: 3, Content: "Future open", DueDate: &tomorrow, Status: StatusOpen},
|
||||
{ID: 4, Content: "No due date", DueDate: nil, Status: StatusOpen},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).WithOverdue().Apply()
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1 task, got %d", len(result))
|
||||
return
|
||||
}
|
||||
if result[0].ID != 1 {
|
||||
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_WithSearch(t *testing.T) {
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Buy groceries"},
|
||||
{ID: 2, Content: "Call the doctor"},
|
||||
{ID: 3, Content: "Review PR for grocery app"},
|
||||
{ID: 4, Content: "Send email"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query string
|
||||
expected []int
|
||||
}{
|
||||
{"exact match", "groceries", []int{1}},
|
||||
{"partial match", "grocer", []int{1, 3}},
|
||||
{"case insensitive", "CALL", []int{2}},
|
||||
{"no match", "xyz", []int{}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := NewFilter(tasks).WithSearch(tt.query).Apply()
|
||||
if len(result) != len(tt.expected) {
|
||||
t.Errorf("expected %d tasks, got %d", len(tt.expected), len(result))
|
||||
return
|
||||
}
|
||||
for i, task := range result {
|
||||
if task.ID != tt.expected[i] {
|
||||
t.Errorf("expected task ID %d at index %d, got %d", tt.expected[i], i, task.ID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_Combined(t *testing.T) {
|
||||
today := time.Now().Truncate(24 * time.Hour)
|
||||
yesterday := today.AddDate(0, 0, -1)
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Important task", TagsAsText: "important", Status: StatusOpen, DueDate: &yesterday},
|
||||
{ID: 2, Content: "Important closed", TagsAsText: "important", Status: StatusClosed, DueDate: &yesterday},
|
||||
{ID: 3, Content: "Regular task", TagsAsText: "regular", Status: StatusOpen, DueDate: &yesterday},
|
||||
{ID: 4, Content: "Important future", TagsAsText: "important", Status: StatusOpen, DueDate: &tomorrow},
|
||||
}
|
||||
|
||||
// Filter: important AND open AND due before today
|
||||
result := NewFilter(tasks).
|
||||
WithTag("important").
|
||||
WithStatus(StatusOpen).
|
||||
WithDueBefore(today).
|
||||
Apply()
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected 1 task, got %d", len(result))
|
||||
return
|
||||
}
|
||||
if result[0].ID != 1 {
|
||||
t.Errorf("expected task ID 1, got %d", result[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_EmptyFilters(t *testing.T) {
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Task 1"},
|
||||
{ID: 2, Content: "Task 2"},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).Apply()
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_EmptyTasks(t *testing.T) {
|
||||
result := NewFilter([]Task{}).WithTag("test").Apply()
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected 0 tasks, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_Performance_1000Tasks(t *testing.T) {
|
||||
// Create 1000 tasks with various attributes
|
||||
tasks := make([]Task, 1000)
|
||||
now := time.Now()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
due := now.AddDate(0, 0, i-500) // -500 to +499 days from now
|
||||
tasks[i] = Task{
|
||||
ID: i + 1,
|
||||
Content: "Task with some content to search through",
|
||||
TagsAsText: "tag1, tag2, tag3",
|
||||
Status: TaskStatus(i % 3),
|
||||
DueDate: &due,
|
||||
}
|
||||
}
|
||||
|
||||
// Mark important tasks
|
||||
for i := 0; i < 100; i++ {
|
||||
tasks[i].TagsAsText = "important, urgent"
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
// Run a complex filter
|
||||
result := NewFilter(tasks).
|
||||
WithTag("important").
|
||||
WithStatus(StatusOpen).
|
||||
WithDueBefore(now).
|
||||
WithSearch("content").
|
||||
Apply()
|
||||
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Performance target: <10ms for 1000 tasks
|
||||
if elapsed > 10*time.Millisecond {
|
||||
t.Errorf("filter took %v, expected <10ms", elapsed)
|
||||
}
|
||||
|
||||
// Verify we got some results
|
||||
if len(result) == 0 {
|
||||
t.Log("No matching tasks found, but performance test passed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_TagsMap(t *testing.T) {
|
||||
// Test that Tags map is also checked
|
||||
tasks := []Task{
|
||||
{ID: 1, Content: "Task 1", Tags: Tags{"important": true}, TagsAsText: ""},
|
||||
{ID: 2, Content: "Task 2", Tags: nil, TagsAsText: "important"},
|
||||
}
|
||||
|
||||
result := NewFilter(tasks).WithTag("important").Apply()
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2 tasks, got %d", len(result))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue