Files
gitea-tea/internal/terminal/table_test.go
T
bircni ea13c9e4f5 refactor: remove features beyond the UX plan
Drop additions that the UX plan does not ask for:

- the interactive label edit form; label edit now requires a flag
- label clone and the skip mode of label creation it needed
- repo rename rewriting the Git remote or tea.repository; it now
  warns that the remote still points to the old name
- the extra "source" remote on clones of pull mirrors
- run artifact download, which duplicates run download --zip
- 256-color and 24-bit label swatches; colors stay ANSI 16
- config list --show-source (config get keeps it)
- the membership and access GETs before team member/repo add and
  remove; Gitea's PUT and DELETE are idempotent

label edit/delete keep --by-name: it is the only way to address a
label whose name is a number that is also another label's ID.

Signed-off-by: bircni <bircni@icloud.com>
2026-10-12 10:36:50 +02:00

165 lines
6.4 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terminal
import (
"strings"
"testing"
"time"
"github.com/mattn/go-runewidth"
"github.com/stretchr/testify/require"
)
func TestTable(t *testing.T) {
now := time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC)
tty := Display{TTY: true, Now: now}
issues := []Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Labels", Flex: true}, {Header: "Updated"}}
issueRows := [][]Cell{
{{Text: "12", Link: "https://example.org/12"}, {Text: "Emoji in title 🚀 works?"}, {}, {Time: now.Add(-2 * time.Hour)}},
{{Text: "1", Link: "https://example.org/1"}, {Text: "Login fails with special characters in password"}, {Text: "bug, priority/high"}, {Time: now.Add(-90 * time.Minute)}},
}
for _, tc := range []struct {
name string
columns []Column
rows [][]Cell
d Display
want string
}{
{"empty", issues, nil, tty, ""},
{"aligns legacy rows without header", nil, [][]Cell{row("7", "Fix login", "open"), row("12", "Crash", "closed")}, tty, "7 Fix login open\n12 Crash closed\n"},
{"counts runes, not bytes", nil, [][]Cell{row("1", "äöü", "x"), row("2", "abcd", "y")}, tty, "1 äöü x\n2 abcd y\n"},
{"counts wide runes twice", nil, [][]Cell{row("1", "修复", "x"), row("2", "abcd", "y")}, tty, "1 修复 x\n2 abcd y\n"},
{"trims empty trailing cells", nil, [][]Cell{row("1", "bug", ""), row("22", "feature", "new work")}, tty, "1 bug\n22 feature new work\n"},
{"pads ragged rows", nil, [][]Cell{row("1"), row("2", "two")}, tty, "1\n2 two\n"},
{
"heads columns and formats cells for people",
issues, issueRows, tty,
"ID TITLE LABELS UPDATED\n" +
"#12 Emoji in title 🚀 works? about 2 hours ago\n" +
"#1 Login fails with special characters in password bug, priority/high about 1 hour ago\n",
},
{
"shrinks the widest flexible column first",
issues, issueRows,
Display{TTY: true, Now: now, Width: 72},
"ID TITLE LABELS UPDATED\n" +
"#12 Emoji in title 🚀 works? about 2 hours ago\n" +
"#1 Login fails with special ch… bug, priority/high about 1 hour ago\n",
},
{
"keeps flexible columns at their floor",
issues, issueRows,
Display{TTY: true, Now: now, Width: 40},
"ID TITLE LABELS UPDATED\n" +
"#12 Emoji i… about 2 hours ago\n" +
"#1 Login f… bug, pr… about 1 hour ago\n",
},
{
"cuts wide runes by display width",
[]Column{{Header: "Title", Flex: true}, {Header: "By"}},
[][]Cell{row("支持中文路径 (support CJK paths)", "alice")},
Display{TTY: true, Width: 20},
"TITLE BY\n支持中文路径… alice\n",
},
{
"writes tab-separated values when piped",
issues, issueRows,
Display{Width: 40, Color: true, Links: true},
"12\tEmoji in title 🚀 works?\t\t2026-10-11T18:00:00Z\n" +
"1\tLogin fails with special characters in password\tbug, priority/high\t2026-10-11T18:30:00Z\n",
},
{
"keeps columns to their output",
[]Column{{Header: "ID"}, {Header: "State", PipedOnly: true}, {Header: "Link", TTYOnly: true}},
[][]Cell{row("1", "open", "→")},
tty,
"ID LINK\n1 →\n",
},
{
"keeps piped-only columns when piped",
[]Column{{Header: "ID"}, {Header: "State", PipedOnly: true}, {Header: "Link", TTYOnly: true}},
[][]Cell{row("1", "open", "→")},
Display{},
"1\topen\n",
},
{
"strips hostile text in both outputs",
nil,
[][]Cell{row("1", "a\x1b]52;c;cHduZWQ=\x07b\x1b[2J\u009b31m‮txt.exe\r\ntail\tend", "open")},
Display{},
"1\ta]52;c;cHduZWQ=b[2J31mtxt.exe tail end\topen\n",
},
{"ignores styles without color", nil, [][]Cell{{{Text: "7", Link: "https://example.org/7"}, {Text: "bug", Color: "ff0000"}, {Text: "open", Bold: true}}}, tty, "7 bug open\n"},
{
"styles after alignment and links only the first column",
[]Column{{Header: "ID"}, {Header: "Name"}, {Header: "State"}},
[][]Cell{{{Text: "7", Link: "https://example.org/7"}, {Text: "bug", Color: "ff0000", Link: "https://example.org/bug"}, {Text: "open", Color: "green", Bold: true}}, row("12", "feature", "closed")},
Display{TTY: true, Color: true, Links: true},
"\x1b[90mID\x1b[0m \x1b[90mNAME\x1b[0m \x1b[90mSTATE\x1b[0m\n" +
"\x1b]8;;https://example.org/7\x1b\\7\x1b]8;;\x1b\\ bug \x1b[1m\x1b[32mopen\x1b[0m\x1b[0m\n" +
"12 feature closed\n",
},
{
"links without color",
nil,
[][]Cell{{{Text: "7", Link: "https://example.org/7"}, {Text: "bug", Muted: true}}},
Display{TTY: true, Links: true},
"\x1b]8;;https://example.org/7\x1b\\7\x1b]8;;\x1b\\ bug\n",
},
{
"leaves hex colors plain",
nil,
[][]Cell{{{Text: "bug", Color: "#ff0000"}, {Text: "old", Muted: true}}},
Display{TTY: true, Color: true},
"bug \x1b[90mold\x1b[0m\n",
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, Table(tc.columns, tc.rows, tc.d))
})
}
}
func TestTableFitsWidthWithWideRunes(t *testing.T) {
titles := []string{"Emoji in title 🚀 works?", "支持中文路径 (support CJK paths)", strings.Repeat("long ", 40)}
rows := make([][]Cell, len(titles))
for i, title := range titles {
rows[i] = row("#1", title, "alice")
}
for _, width := range []int{30, 50, 80} {
for _, line := range strings.Split(strings.TrimSuffix(Table([]Column{{}, {Flex: true}, {}}, rows, Display{TTY: true, Width: width}), "\n"), "\n") {
require.LessOrEqual(t, runewidth.StringWidth(line), width, line)
}
}
}
func TestSafeText(t *testing.T) {
for _, tc := range []struct{ name, in, text, line string }{
{"plain", "fix login", "fix login", "fix login"},
{"keeps tabs and line breaks", "a\tb\r\nc\nd", "a\tb\nc\nd", "a b c d"},
{"drops lone carriage returns", "safe\rFAKE", "safeFAKE", "safeFAKE"},
{
"strips hostile body",
"Steps:\r\n\x1b]52;c;cHduZWQ=\x07run\x1b[2J\u009b31m‮​it\x7f\n\x00done",
"Steps:\n]52;c;cHduZWQ=run[2J31mit\ndone",
"Steps: ]52;c;cHduZWQ=run[2J31mit done",
},
} {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.text, SafeText(tc.in))
require.Equal(t, tc.line, SafeLine(tc.in))
})
}
}
// row returns unstyled cells.
func row(texts ...string) []Cell {
cells := make([]Cell, len(texts))
for i, text := range texts {
cells[i].Text = text
}
return cells
}