Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions internal/output/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2356,3 +2356,162 @@ func TestRenderDataStripsOSCFromTopLevelString(t *testing.T) {
assert.NotContains(t, output, "\x1b]52", "OSC 52 clipboard injection must be stripped")
assert.Contains(t, output, "safetext")
}

// =============================================================================
// Generic Table Date Formatting Tests
// =============================================================================

func TestStyledTableFormatsDateColumns(t *testing.T) {
// Use date-only format to guarantee the absolute-date branch
// (RFC3339 timestamps would hit relative formatting if within 7 days of now).
data := []any{
map[string]any{
"id": float64(1),
"name": "Project A",
"created_at": "2024-01-15",
},
}
var buf bytes.Buffer
w := New(Options{Format: FormatStyled, Writer: &buf})
err := w.OK(data)
require.NoError(t, err)

output := buf.String()
assert.NotContains(t, output, "2024-01-15",
"generic table should not show raw date string")
assert.Contains(t, output, "Jan 15, 2024",
"generic table should show human-readable date")
}

func TestMarkdownTableFormatsDateColumns(t *testing.T) {
// Use date-only format to guarantee the absolute-date branch.
data := []any{
map[string]any{
"id": float64(1),
"name": "Project A",
"created_at": "2024-01-15",
},
}
var buf bytes.Buffer
w := New(Options{Format: FormatMarkdown, Writer: &buf})
err := w.OK(data)
require.NoError(t, err)

output := buf.String()
assert.NotContains(t, output, "2024-01-15",
"markdown table should not show raw date string")
assert.Contains(t, output, "Jan 15, 2024",
"markdown table should show human-readable date")
}

func TestSelectColumnsUsesFormattedDateWidth(t *testing.T) {
// Verify that selectColumns measures the formatted date width (short)
// rather than raw ISO8601 width (30 chars)
r := &Renderer{width: 60}
cols := []column{
{key: "name", header: "Name", priority: 2},
{key: "created_at", header: "Created", priority: 8},
}
data := []map[string]any{
{"name": "Test", "created_at": "2024-01-15"},
}
selected := r.selectColumns(cols, data)

// With 60-char width, both columns should fit when dates are formatted
// (formatted date is ~12 chars vs raw ISO8601 at 20+ chars)
require.Len(t, selected, 2, "both columns should fit with formatted date widths")

// Verify the width is based on the formatted value, not the raw timestamp
for _, col := range selected {
if col.key == "created_at" {
assert.Less(t, col.width, 20,
"date column width should reflect formatted date, not raw ISO8601")
}
}
}

// =============================================================================
// updated_at Omission in Generic Tables
// =============================================================================

func TestGenericTableOmitsUpdatedAt(t *testing.T) {
data := []any{
map[string]any{
"id": float64(1),
"name": "Test Item",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-02-20T15:00:00Z",
},
}
var buf bytes.Buffer
w := New(Options{Format: FormatStyled, Writer: &buf})
err := w.OK(data)
require.NoError(t, err)

output := buf.String()
assert.Contains(t, output, "Created",
"generic table should show Created column")
assert.NotContains(t, output, "Updated",
"generic table should not show Updated column")
}

// =============================================================================
// HTML Stripping in formatCell
// =============================================================================

func TestFormatCellStripsHTML(t *testing.T) {
tests := []struct {
name string
input string
contains string
excludes string
}{
{
name: "bold HTML",
input: "<div><strong>Hello</strong> world</div>",
contains: "Hello",
excludes: "<strong>",
},
{
name: "paragraph tags",
input: "<p>First paragraph</p><p>Second paragraph</p>",
contains: "First paragraph",
excludes: "<p>",
},
{
name: "link HTML",
input: `<a href="https://example.com">Click here</a>`,
contains: "Click here",
excludes: "<a ",
},
{
name: "plain text passthrough",
input: "just plain text",
contains: "just plain text",
excludes: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatCell(tt.input)
assert.Contains(t, result, tt.contains)
if tt.excludes != "" {
assert.NotContains(t, result, tt.excludes,
"formatCell should not contain raw HTML tags")
}
})
}
}

func TestFormatTableCellDelegatesToFormatDateValue(t *testing.T) {
timestamp := "2024-01-15T10:00:00Z"
assert.Equal(t,
formatDateValue("created_at", timestamp),
formatTableCell("created_at", timestamp),
"formatTableCell should produce the same result as formatDateValue for date columns")
assert.Equal(t,
formatDateValue("name", "Test"),
formatTableCell("name", "Test"),
"formatTableCell should produce the same result as formatDateValue for non-date columns")
}
16 changes: 13 additions & 3 deletions internal/output/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ var skipColumns = map[string]bool{
"bucket": true,
"creator": true,
"parent": true,
"updated_at": true,
"dock": true,
"inherits_status": true,
"url": true,
Expand Down Expand Up @@ -426,7 +427,7 @@ func (r *Renderer) renderTable(b *strings.Builder, data []map[string]any) {
for _, item := range data {
row := make([]string, len(columns))
for i, col := range columns {
cell := formatCell(item[col.key])
cell := formatTableCell(col.key, item[col.key])
if r.styled && (col.key == "title" || col.key == "name") {
if url, ok := item["app_url"].(string); ok && url != "" {
cell = richtext.Hyperlink(cell, url)
Expand Down Expand Up @@ -497,7 +498,7 @@ func (r *Renderer) selectColumns(cols []column, data []map[string]any) []column
for i := range cols {
cols[i].width = lipgloss.Width(cols[i].header)
for _, row := range data {
cellWidth := lipgloss.Width(formatCell(row[cols[i].key]))
cellWidth := lipgloss.Width(formatTableCell(cols[i].key, row[cols[i].key]))
if cellWidth > cols[i].width {
cols[i].width = cellWidth
}
Expand Down Expand Up @@ -667,6 +668,9 @@ func formatCell(val any) string {
return ""
case string:
v = ansi.Strip(v)
if richtext.IsHTML(v) {
v = richtext.HTMLToMarkdown(v)
}
if strings.ContainsAny(v, "\n\r") {
v = strings.Join(strings.Fields(v), " ")
}
Expand Down Expand Up @@ -739,6 +743,12 @@ func isURL(s string) bool {
!strings.ContainsRune(s, ' ')
}

// formatTableCell formats a value for table cell display. Date columns get
// human-readable formatting via formatDateValue; everything else uses formatCell.
func formatTableCell(key string, val any) string {
return formatDateValue(key, val)
}

// formatDateValue formats date fields in a human-readable way.
// For date columns (created_at, updated_at, due_on, due_date), it converts
// ISO8601 timestamps to a more readable format.
Expand Down Expand Up @@ -933,7 +943,7 @@ func (r *MarkdownRenderer) renderTable(b *strings.Builder, data []map[string]any
for _, item := range data {
var cells []string
for _, col := range cols {
cell := formatCell(item[col.key])
cell := formatTableCell(col.key, item[col.key])
// Escape pipe characters in cell content
cell = strings.ReplaceAll(cell, "|", "\\|")
cells = append(cells, cell)
Expand Down
Loading