package parse import ( "strings" "testing" ) func sp(s string) *string { return &s } func TestParse(t *testing.T) { tests := []struct { name string input string wantBody string wantGlyph string wantTitle *string wantDesc *string wantTime *string wantTags []string wantCard *string wantErrSub string }{ // Glyph detection {"plain note", "hello world", "hello world", "note", nil, nil, nil, nil, nil, ""}, {"dash todo", "- deploy nginx", "deploy nginx", "todo", nil, nil, nil, nil, nil, ""}, {"unicode todo", "▸ deploy nginx", "deploy nginx", "todo", nil, nil, nil, nil, nil, ""}, {"star event", "* dentist", "dentist", "event", nil, nil, nil, nil, nil, ""}, {"unicode event", "◇ dentist", "dentist", "event", nil, nil, nil, nil, nil, ""}, // Time anchor {"with time", "meeting @14:00", "meeting", "note", nil, nil, sp("14:00"), nil, nil, ""}, {"time at start", "@9:30 standup", "standup", "note", nil, nil, sp("9:30"), nil, nil, ""}, {"invalid hours", "meeting @25:00", "", "", nil, nil, nil, nil, nil, "invalid time"}, {"invalid minutes", "meeting @14:60", "", "", nil, nil, nil, nil, nil, "invalid time"}, // Tags {"single tag", "deploy #ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, ""}, {"multiple tags", "deploy #ops #infra", "deploy", "note", nil, nil, nil, []string{"ops", "infra"}, nil, ""}, {"duplicate tags", "deploy #ops #ops", "deploy", "note", nil, nil, nil, []string{"ops"}, nil, ""}, {"tag with hyphen", "task #dev-ops", "task", "note", nil, nil, nil, []string{"dev-ops"}, nil, ""}, // Card suffix {"caret card", "trick #nginx ^card", "trick", "note", nil, nil, nil, []string{"nginx"}, sp("snippet"), ""}, {"caret c", "trick ^c", "trick", "note", nil, nil, nil, nil, sp("snippet"), ""}, {"caret template", "deploy ${host} ^template", "deploy ${host}", "note", nil, nil, nil, nil, sp("template"), ""}, {"caret snippet explicit", "trick ^snippet", "trick", "note", nil, nil, nil, nil, sp("snippet"), ""}, {"invalid card type", "thing ^bogus", "", "", nil, nil, nil, nil, nil, "invalid card type"}, // Combined {"full input", "- deploy nginx to staging @15:00 #ops", "deploy nginx to staging", "todo", nil, nil, sp("15:00"), []string{"ops"}, nil, ""}, {"full with card", "figured out the proxy_pass trick #nginx ^card", "figured out the proxy_pass trick", "note", nil, nil, nil, []string{"nginx"}, sp("snippet"), ""}, // Title {"title with body", "|nginx trick\nproxy_pass trailing slash #ops", "proxy_pass trailing slash", "note", sp("nginx trick"), nil, nil, []string{"ops"}, nil, ""}, {"no title", "no pipe here #ops", "no pipe here", "note", nil, nil, nil, []string{"ops"}, nil, ""}, {"todo with title", "- |deploy staging\nrebuild docker #ops", "rebuild docker", "todo", sp("deploy staging"), nil, nil, []string{"ops"}, nil, ""}, {"title only", "|title only", "", "note", sp("title only"), nil, nil, nil, nil, ""}, {"title and desc", "|title // description #ops\nbody here", "body here", "note", sp("title"), sp("description"), nil, []string{"ops"}, nil, ""}, {"todo title desc", "- |deploy staging // rebuild and push #ops", "", "todo", sp("deploy staging"), sp("rebuild and push"), nil, []string{"ops"}, nil, ""}, // Description without title {"leading desc", "// leading desc\nbody content", "body content", "note", nil, sp("leading desc"), nil, nil, nil, ""}, {"inline desc", "body text // inline desc", "body text", "note", nil, sp("inline desc"), nil, nil, nil, ""}, {"url no split", "http://example.com // should not split", "http://example.com // should not split", "note", nil, nil, nil, nil, nil, ""}, // Edge cases {"empty input", "", "", "", nil, nil, nil, nil, nil, "empty"}, {"only glyph", "-", "", "", nil, nil, nil, nil, nil, "empty body"}, {"only modifiers", "#ops @14:00", "", "", nil, nil, nil, nil, nil, "empty body"}, {"whitespace only", " ", "", "", nil, nil, nil, nil, nil, "empty"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := Parse(tt.input) if tt.wantErrSub != "" { if err == nil { t.Fatalf("expected error containing %q, got nil", tt.wantErrSub) } if !strings.Contains(err.Error(), tt.wantErrSub) { t.Fatalf("expected error containing %q, got %q", tt.wantErrSub, err.Error()) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got.Body != tt.wantBody { t.Errorf("body: got %q, want %q", got.Body, tt.wantBody) } if got.Glyph != tt.wantGlyph { t.Errorf("glyph: got %q, want %q", got.Glyph, tt.wantGlyph) } if !ptrEq(got.Title, tt.wantTitle) { t.Errorf("title: got %v, want %v", strPtr(got.Title), strPtr(tt.wantTitle)) } if !ptrEq(got.Description, tt.wantDesc) { t.Errorf("description: got %v, want %v", strPtr(got.Description), strPtr(tt.wantDesc)) } if !ptrEq(got.TimeAnchor, tt.wantTime) { t.Errorf("time_anchor: got %v, want %v", strPtr(got.TimeAnchor), strPtr(tt.wantTime)) } if !tagsEq(got.Tags, tt.wantTags) { t.Errorf("tags: got %v, want %v", got.Tags, tt.wantTags) } if !ptrEq(got.CardSuffix, tt.wantCard) { t.Errorf("card_suffix: got %v, want %v", strPtr(got.CardSuffix), strPtr(tt.wantCard)) } }) } } func ptrEq(a, b *string) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } return *a == *b } func strPtr(p *string) string { if p == nil { return "" } return *p } func tagsEq(a, b []string) bool { if len(a) == 0 && len(b) == 0 { return true } if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }