mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
tea pr list prints a header, a "Showing N of M" line and the ID, TITLE,
BRANCH, STATE, AUTHOR and CREATED columns on a terminal, fit to its
width, and tab-separated rows when piped. Fork heads read
OWNER:BRANCH, drafts and merged pull requests have their own state,
and STATE appears on a terminal only when --state is not open. New
filters: --state merged, -d/--draft, -H/--head, -a/--assignee,
-S/--search and -w/--web, plus the gh shorthands -s, -A, -B, -l and
-m; --milestone takes a title or an ID, and @me works for --author and
--assignee. An empty list says so on stderr. draft, merged_at and
merged_by are exported in JSON, and reviewers, reviewer_teams and
merged_by, which Gitea's list leaves out, are read when --json names
them.
issue view and pr view render through terminal.View. Pull requests
show Draft and Merged badges, "alice merged into main from fix" with
the merge time, each reviewer's latest state ("bob (Approved), carol
(Requested)"), a checks summary pointing at tea pr checks, and why a
pull request cannot be merged. --comments includes reviews without a
body ("dave approved these changes") and comment IDs. Issues show
their due date and ref, and both kinds hint at --comments when there
are comments. pr view accepts #N, URLs and branch names, and its JSON
adds review_decision. Check links are absolute.
Signed-off-by: bircni <bircni@icloud.com>
435 lines
19 KiB
Go
435 lines
19 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dev/tea/internal/settings"
|
|
"gitea.dev/tea/internal/terminal"
|
|
"github.com/mattn/go-runewidth"
|
|
"github.com/spf13/cobra"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type recordingPager struct {
|
|
command string
|
|
input string
|
|
calls int
|
|
}
|
|
|
|
func (p *recordingPager) Page(_ context.Context, command string, input io.Reader, streams terminal.Streams) error {
|
|
p.calls++
|
|
p.command = command
|
|
data, err := io.ReadAll(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.input = string(data)
|
|
_, err = io.WriteString(streams.Out, "PAGED:"+string(data))
|
|
return err
|
|
}
|
|
|
|
// pagedDeps returns serverDeps on a terminal stdout with a recording pager,
|
|
// extra environment variables and the given config settings.
|
|
func pagedDeps(t *testing.T, serverURL string, env map[string]string, config ...string) (Dependencies, *bytes.Buffer, *bytes.Buffer, *recordingPager) {
|
|
t.Helper()
|
|
deps, out, diagnostics := testDeps(t)
|
|
pager := &recordingPager{}
|
|
deps.Pager = pager
|
|
deps.Streams.OutTTY = true
|
|
deps.Getenv = serverEnv(serverURL, env)
|
|
for i := 0; i < len(config); i += 2 {
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", config[i], config[i+1]}))
|
|
}
|
|
diagnostics.Reset()
|
|
return deps, out, diagnostics, pager
|
|
}
|
|
|
|
func TestLabelListAppliesColorAndPagesWithNoInput(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
require.Equal(t, "/api/v1/repos/alice/project/labels", r.URL.Path)
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":"Defect"},{"id":2,"name":"docs","color":"00ff00","description":"Docs"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, diagnostics, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager", "COLORTERM": "truecolor"}, "color", "always")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, "fake-pager", pager.command)
|
|
require.Contains(t, pager.input, "\x1b[38;2;255;0;0mbug\x1b[0m")
|
|
require.Contains(t, pager.input, "\x1b[38;2;0;255;0mdocs\x1b[0m")
|
|
require.Equal(t, "PAGED:"+pager.input, out.String())
|
|
require.Empty(t, diagnostics.String())
|
|
}
|
|
|
|
func TestIssueListRespectsColorNeverAndStillPages(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"number":1,"title":"One","state":"open"},{"number":2,"title":"Two","state":"closed"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _, pager := pagedDeps(t, server.URL, nil, "color", "never", "pager", "configured-pager")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, "configured-pager", pager.command)
|
|
require.NotContains(t, pager.input, "\x1b[")
|
|
require.Contains(t, pager.input, "open")
|
|
require.Contains(t, pager.input, "closed")
|
|
require.Equal(t, "PAGED:"+pager.input, out.String())
|
|
}
|
|
|
|
func TestIssueViewJSONDoesNotPage(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = fmt.Fprint(w, `{"number":7,"title":"Title","html_url":"https://example.org/alice/project/issues/7","body":"line1\nline2\nline3"}`)
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "fake-pager"})
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"issue", "view", "7", "-R", "alice/project", "--json=number,title", "--no-input"}))
|
|
require.Zero(t, pager.calls)
|
|
require.JSONEq(t, `{"number":7,"title":"Title"}`, withoutANSI(out.String()))
|
|
}
|
|
|
|
func TestConfiguredPagerOverridesPagerEnvironment(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"number":1,"title":"One"},{"number":2,"title":"Two"}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, _, _, pager := pagedDeps(t, server.URL, map[string]string{"PAGER": "env-pager"})
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "env-pager", pager.command)
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "set", "pager", "configured-pager"}))
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"pr", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "configured-pager", pager.command)
|
|
}
|
|
|
|
func TestTeaPagerAndColorEnvironmentOverrides(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":""},{"id":2,"name":"docs","color":"00ff00","description":""}]`))
|
|
}))
|
|
defer server.Close()
|
|
env := map[string]string{"TEA_PAGER": "tea-pager", "PAGER": "generic-pager", "TEA_COLOR": "always"}
|
|
deps, out, _, pager := pagedDeps(t, server.URL, env, "pager", "configured-pager", "color", "never")
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "tea-pager", pager.command)
|
|
require.Contains(t, pager.input, "\x1b[38;5;196mbug\x1b[0m")
|
|
out.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "pager", "--show-source", "--json"}))
|
|
require.JSONEq(t, `{"pager":{"value":"tea-pager","source":"TEA_PAGER"}}`, withoutANSI(out.String()))
|
|
out.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"config", "get", "color", "--show-source", "--json"}))
|
|
require.JSONEq(t, `{"color":{"value":"always","source":"TEA_COLOR"}}`, withoutANSI(out.String()))
|
|
}
|
|
|
|
func TestColorAutoUsesStdoutTTY(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"id":1,"name":"bug","color":"ff0000","description":""}]`))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, _ := serverDeps(t, server.URL)
|
|
deps.Streams.OutTTY = false
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Equal(t, "1\tbug\tff0000\t\n", out.String())
|
|
|
|
out.Reset()
|
|
deps.Streams.OutTTY = true
|
|
require.NoError(t, Execute(context.Background(), deps, []string{"label", "list", "-R", "alice/project", "--no-input"}))
|
|
require.Contains(t, out.String(), "\x1b[38;5;196mbug\x1b[0m")
|
|
}
|
|
|
|
func TestMultiItemHumanOutputPagesOnce(t *testing.T) {
|
|
issue := func(n int) string {
|
|
return fmt.Sprintf(`{"number":%d,"title":"Issue %d","state":"closed","html_url":"https://example.org/i/%d"}`, n, n, n)
|
|
}
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/api/v1/repos/alice/project/milestones/3":
|
|
_, _ = w.Write([]byte(`{"id":3,"title":"v1"}`))
|
|
case "/api/v1/repos/alice/project/issues":
|
|
_, _ = w.Write([]byte("[" + issue(1) + "," + issue(2) + "]"))
|
|
case "/api/v1/repos/alice/project/issues/1", "/api/v1/repos/alice/project/issues/2":
|
|
_, _ = w.Write([]byte(issue(int(r.URL.Path[len(r.URL.Path)-1] - '0'))))
|
|
case "/api/v1/repos/alice/project":
|
|
_, _ = w.Write([]byte(`{"full_name":"alice/project","default_branch":"main"}`))
|
|
case "/api/v1/repos/alice/project/branches":
|
|
_, _ = w.Write([]byte(`[{"name":"main","commit":{"id":"abc"}},{"name":"dev","commit":{"id":"def"}}]`))
|
|
case "/api/v1/repos/alice/project/issues/5/comments":
|
|
_, _ = w.Write([]byte(`[{"id":1,"body":"one","user":{"login":"alice"}},{"id":2,"body":"two","user":{"login":"bob"}}]`))
|
|
default:
|
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
for _, tc := range []struct {
|
|
args []string
|
|
want string
|
|
}{
|
|
{[]string{"milestone", "issue", "list", "3"}, "1 Issue 1 closed\n2 Issue 2 closed\n"},
|
|
{[]string{"issue", "close", "1", "2"}, "1 Issue 1 closed\n2 Issue 2 closed\n"},
|
|
{[]string{"branch", "list"}, "Showing 2 of 2 branches in alice/project\n\nNAME COMMIT PROTECTED UPDATED MESSAGE\nmain (default) abc\ndev def\n"},
|
|
{[]string{"issue", "comment", "list", "5"}, "1 alice\none\n\n2 bob\ntwo\n\n"},
|
|
} {
|
|
t.Run(tc.args[0]+" "+tc.args[1], func(t *testing.T) {
|
|
deps, out, _, pager := pagedDeps(t, server.URL, nil, "pager", "fake-pager", "color", "never", "hyperlinks", "never")
|
|
require.NoError(t, Execute(context.Background(), deps, append(tc.args, "-R", "alice/project", "--yes")))
|
|
require.Equal(t, 1, pager.calls)
|
|
require.Equal(t, tc.want, pager.input)
|
|
require.Equal(t, "PAGED:"+tc.want, out.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListsHintWhenMorePagesExist(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
size, total := min(limit, 50), 120
|
|
items := []string{}
|
|
for id := (page-1)*size + 1; id <= min(page*size, total); id++ {
|
|
items = append(items, fmt.Sprintf(`{"id":%d,"username":"org%d","full_name":"owner/repo%d"}`, id, id, id))
|
|
}
|
|
w.Header().Set("X-Total-Count", strconv.Itoa(total))
|
|
_, _ = w.Write([]byte("[" + strings.Join(items, ",") + "]"))
|
|
}))
|
|
defer server.Close()
|
|
deps, out, diagnostics := serverDeps(t, server.URL)
|
|
for _, args := range [][]string{{"org", "list"}, {"repo", "list"}, {"issue", "list", "-R", "alice/project"}, {"pr", "list", "-R", "alice/project"}} {
|
|
out.Reset()
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--limit", "100")))
|
|
require.Len(t, strings.Split(strings.TrimSpace(out.String()), "\n"), 100, args)
|
|
require.Equal(t, "more results available; use --page 2\n", diagnostics.String(), args)
|
|
out.Reset()
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--limit", "100", "--page", "2")))
|
|
require.Len(t, strings.Split(strings.TrimSpace(out.String()), "\n"), 20, args)
|
|
require.Empty(t, diagnostics.String(), args)
|
|
diagnostics.Reset()
|
|
require.NoError(t, Execute(context.Background(), deps, append(args, "--json=id")))
|
|
require.Empty(t, diagnostics.String(), args)
|
|
}
|
|
}
|
|
|
|
func TestListsFitTheTerminalAndSplitWhenPiped(t *testing.T) {
|
|
server := newAuthenticatedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = w.Write([]byte(`[{"number":6,"title":"A very long issue title that keeps going and going to see how list output handles wrapping","state":"open","user":{"login":"alice"}},` +
|
|
`{"number":5,"title":"支持中文路径 (support CJK paths)","state":"open","user":{"login":""}}]`))
|
|
}))
|
|
defer server.Close()
|
|
for _, group := range []string{"issue"} {
|
|
t.Run(group, func(t *testing.T) {
|
|
deps, out, _ := serverDeps(t, server.URL)
|
|
args := []string{group, "list", "-R", "alice/project"}
|
|
require.NoError(t, Execute(context.Background(), deps, args))
|
|
require.Equal(t, "6\tA very long issue title that keeps going and going to see how list output handles wrapping\topen\talice\n"+
|
|
"5\t支持中文路径 (support CJK paths)\topen\t\n", out.String())
|
|
|
|
out.Reset()
|
|
deps.Streams.OutTTY, deps.Streams.Width = true, 50
|
|
deps.Getenv = serverEnv(server.URL, map[string]string{"NO_COLOR": "1"})
|
|
require.NoError(t, Execute(context.Background(), deps, args))
|
|
require.Equal(t, "ID TITLE STATE AUTHOR\n"+
|
|
"#6 A very long issue title that k… open alice\n"+
|
|
"#5 支持中文路径 (support CJK path… open\n", stripLinks(out.String()))
|
|
for _, line := range strings.Split(strings.TrimSpace(out.String()), "\n") {
|
|
require.LessOrEqual(t, runewidth.StringWidth(stripLinks(line)), 50, line)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// stripLinks removes OSC 8 hyperlinks from rendered output.
|
|
func stripLinks(s string) string {
|
|
for {
|
|
start := strings.Index(s, "\x1b]8;")
|
|
if start < 0 {
|
|
return s
|
|
}
|
|
end := strings.Index(s[start:], "\x1b\\")
|
|
s = s[:start] + s[start+end+2:]
|
|
}
|
|
}
|
|
|
|
// outputApplication returns an application over deps and a command writing to its streams.
|
|
func outputApplication(deps Dependencies, json string) (*application, *cobra.Command) {
|
|
deps.Now = func() time.Time { return time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC) }
|
|
a := &application{deps: deps, options: options{json: json}}
|
|
cmd := &cobra.Command{Use: "list"}
|
|
cmd.SetOut(deps.Streams.Out)
|
|
cmd.SetErr(deps.Streams.ErrOut)
|
|
cmd.SetContext(context.Background())
|
|
return a, cmd
|
|
}
|
|
|
|
type listed struct {
|
|
Number int64 `json:"number"`
|
|
Title string `json:"title"`
|
|
Updated time.Time `json:"updated"`
|
|
}
|
|
|
|
var listedSpec = listSpec[listed]{
|
|
Columns: []terminal.Column{{Header: "ID", Prefix: "#"}, {Header: "Title", Flex: true}, {Header: "Updated"}},
|
|
Row: func(item listed) []terminal.Cell {
|
|
return []terminal.Cell{{Text: strconv.FormatInt(item.Number, 10)}, {Text: item.Title}, {Time: item.Updated}}
|
|
},
|
|
Empty: "no open issues in alice/x",
|
|
Context: "open issues in alice/x",
|
|
}
|
|
|
|
func TestWriteTable(t *testing.T) {
|
|
updated := time.Date(2026, 10, 11, 18, 0, 0, 0, time.UTC)
|
|
items := []listed{{12, "Emoji in title 🚀", updated}, {1, "Login fails", updated}}
|
|
for _, tc := range []struct {
|
|
name string
|
|
tty bool
|
|
json string
|
|
items []listed
|
|
page listPage
|
|
out, stderr string
|
|
pagedHeadings bool
|
|
}{
|
|
{
|
|
name: "context line and header on a terminal", tty: true, items: items, page: listPage{Number: 1, More: true, Total: 112, Counted: true},
|
|
out: "Showing 2 of 112 open issues in alice/x · next: --page 2\n\n" +
|
|
"ID TITLE UPDATED\n#12 Emoji in title 🚀 about 2 hours ago\n#1 Login fails about 2 hours ago\n",
|
|
},
|
|
{
|
|
name: "counts a single page without a total", tty: true, items: items[:1], page: listPage{Number: 1},
|
|
out: "Showing 1 of 1 open issues in alice/x\n\nID TITLE UPDATED\n#12 Emoji in title 🚀 about 2 hours ago\n",
|
|
},
|
|
{
|
|
name: "tab-separated with the page hint on stderr when piped", items: items, page: listPage{Number: 2, More: true},
|
|
out: "12\tEmoji in title 🚀\t2026-10-11T18:00:00Z\n1\tLogin fails\t2026-10-11T18:00:00Z\n",
|
|
stderr: "more results available; use --page 3\n",
|
|
},
|
|
{name: "empty message on a terminal", tty: true, page: listPage{Number: 1}, stderr: "no open issues in alice/x\n"},
|
|
{name: "nothing when empty and piped", page: listPage{Number: 1}},
|
|
{name: "JSON", tty: true, json: "number", items: items[:1], page: listPage{Number: 1, More: true}, out: "[\n {\n \"number\": 12\n }\n]\n"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
deps, out, diagnostics := testDeps(t)
|
|
deps.Streams.OutTTY = tc.tty
|
|
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
|
a, cmd := outputApplication(deps, tc.json)
|
|
require.NoError(t, writeTable(a, cmd, tc.items, listedSpec, tc.page))
|
|
require.Equal(t, tc.out, out.String())
|
|
require.Equal(t, tc.stderr, diagnostics.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteView(t *testing.T) {
|
|
view := func(item listed) terminal.View {
|
|
return terminal.View{
|
|
Title: item.Title, ID: fmt.Sprintf("#%d", item.Number), Badges: []terminal.Cell{{Text: "Open", Color: "green"}},
|
|
Meta: "alice opened " + terminal.RelativeTime(time.Date(2026, 10, 11, 20, 0, 0, 0, time.UTC), item.Updated), Body: "Steps", Kind: "issue", URL: "https://example.org/1",
|
|
}
|
|
}
|
|
item := listed{1, "Login fails", time.Date(2026, 10, 11, 18, 0, 0, 0, time.UTC)}
|
|
for _, tc := range []struct {
|
|
name, json, want string
|
|
tty bool
|
|
}{
|
|
{name: "terminal", tty: true, want: "Login fails #1\nOpen • alice opened about 2 hours ago\n\nSteps\n\nView this issue on Gitea: https://example.org/1\n"},
|
|
{name: "piped", want: "title:\tLogin fails\nurl:\thttps://example.org/1\n--\nSteps\n"},
|
|
{name: "JSON", tty: true, json: "title", want: "{\n \"title\": \"Login fails\"\n}\n"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
deps, out, _ := testDeps(t)
|
|
deps.Streams.OutTTY = tc.tty
|
|
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
|
require.NoError(t, deps.Settings.Update(settings.TargetPreferences, func(s *settings.Snapshot) error {
|
|
s.Preferences.Markdown, s.Preferences.Hyperlinks = "raw", "never"
|
|
return nil
|
|
}))
|
|
a, cmd := outputApplication(deps, tc.json)
|
|
require.NoError(t, writeView(a, cmd, item, view))
|
|
require.Equal(t, tc.want, out.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMutationFeedback(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, json, out, stderr string
|
|
tty bool
|
|
}{
|
|
{name: "terminal", tty: true, out: "https://example.org/12\n", stderr: "✓ Created issue alice/x#12 (Fix[2J login)\n! Issue alice/x#12 (Fix[2J login) is already closed\n"},
|
|
{name: "piped", out: "https://example.org/12\n"},
|
|
{name: "JSON", tty: true, json: "number"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
deps, out, diagnostics := testDeps(t)
|
|
deps.Streams.OutTTY = tc.tty
|
|
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
|
a, cmd := outputApplication(deps, tc.json)
|
|
ref := refLabel("alice/x", 12, "Fix\x1b[2J login")
|
|
a.done(cmd, "Created issue %s", ref)
|
|
a.warn(cmd, "Issue %s is already closed", ref)
|
|
require.NoError(t, a.printURL(cmd, "https://example.org/12"))
|
|
require.Equal(t, tc.out, out.String())
|
|
require.Equal(t, tc.stderr, diagnostics.String())
|
|
})
|
|
}
|
|
deps, _, diagnostics := testDeps(t)
|
|
deps.Streams.OutTTY = true
|
|
a, cmd := outputApplication(deps, "")
|
|
a.done(cmd, "Closed issue %s", "alice/x#5 (Bug)")
|
|
a.warn(cmd, "nothing to do")
|
|
require.Equal(t, "\x1b[32m✓\x1b[0m Closed issue alice/x#5 (Bug)\n\x1b[33m!\x1b[0m nothing to do\n", diagnostics.String())
|
|
}
|
|
|
|
func TestBatchDoneReportsEachSuccess(t *testing.T) {
|
|
run := func(n int64) (listed, error) {
|
|
if n == 2 {
|
|
return listed{}, errors.New("not found")
|
|
}
|
|
return listed{Number: n, Title: fmt.Sprintf("Issue %d", n)}, nil
|
|
}
|
|
label := func(n int64) string { return fmt.Sprintf("#%d", n) }
|
|
report := func(item listed) string { return "Closed issue " + refLabel("alice/x", item.Number, item.Title) }
|
|
for _, tc := range []struct {
|
|
name, json, out, stderr string
|
|
tty bool
|
|
}{
|
|
{name: "terminal", tty: true, stderr: "✓ Closed issue alice/x#1 (Issue 1)\n✓ Closed issue alice/x#3 (Issue 3)\n"},
|
|
{name: "piped"},
|
|
{name: "JSON", json: "number", out: `[{"number":1},{"number":3}]` + "\n"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
deps, out, diagnostics := testDeps(t)
|
|
deps.Streams.OutTTY = tc.tty
|
|
deps.Getenv = envFunc(map[string]string{"NO_COLOR": "1"})
|
|
a, cmd := outputApplication(deps, tc.json)
|
|
err := batchDone(a, cmd, []int64{1, 2, 3}, label, run, report)
|
|
require.EqualError(t, err, "#2: not found")
|
|
require.Equal(t, tc.out, out.String())
|
|
require.Equal(t, tc.stderr, diagnostics.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShowing(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
shown int
|
|
page listPage
|
|
want string
|
|
}{
|
|
{30, listPage{Number: 1, More: true, Total: 112, Counted: true}, "Showing 30 of 112 open issues · next: --page 2"},
|
|
{11, listPage{Number: 1, Total: 11, Counted: true}, "Showing 11 of 11 open issues"},
|
|
{11, listPage{Number: 1}, "Showing 11 of 11 open issues"},
|
|
{30, listPage{Number: 1, More: true}, "Showing 30 open issues · next: --page 2"},
|
|
{5, listPage{Number: 3}, "Showing 5 open issues"},
|
|
} {
|
|
require.Equal(t, tc.want, showing(tc.shown, tc.page, "open issues"), "%+v", tc.page)
|
|
}
|
|
}
|