Files
gitea-tea/internal/terminal/streams_test.go
T
bircni 50a604f169 refactor: remove helpers the new output and prompts left unused
Drop writeDownload, terminal.Require, terminal.Confirm, terminal.Row,
terminal.Record and Pulls.FindOpenByHead, which no command calls anymore,
and point their tests at the code that replaced them.

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

203 lines
7.7 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terminal
import (
"bytes"
"context"
"errors"
"io"
"strings"
"testing"
"charm.land/huh/v2"
"gitea.dev/tea/internal/clierr"
"github.com/stretchr/testify/require"
)
// testPrompter returns a form prompter reading in and the buffer its forms render to.
func testPrompter(in io.Reader) (*FormPrompter, *bytes.Buffer) {
rendered := &bytes.Buffer{}
return &FormPrompter{Streams: Streams{In: in, Out: &bytes.Buffer{}, ErrOut: rendered, InTTY: true, ErrTTY: true}}, rendered
}
// accessiblePrompter is testPrompter in accessible mode, which renders field titles as plain text.
func accessiblePrompter(t *testing.T, input string) (*FormPrompter, *bytes.Buffer) {
t.Helper()
t.Setenv("TERM", "dumb")
return testPrompter(strings.NewReader(input))
}
func noEdit(context.Context, string) (Draft, error) { return Draft{}, nil }
type fakePrompt struct{ inputs int }
func (f *fakePrompt) Input(context.Context, string) (string, error) {
f.inputs++
return "provided", nil
}
func (f *fakePrompt) SecretInput(context.Context, string) (string, error) { return "", nil }
func (f *fakePrompt) Confirm(context.Context, string) (bool, error) { return false, nil }
func TestPromptPolicy(t *testing.T) {
for _, tc := range []struct {
policy Policy
streams Streams
expected bool
}{
{Policy{Enabled: true}, Streams{InTTY: true, ErrTTY: true}, true},
{Policy{Enabled: true}, Streams{InTTY: true, OutTTY: true}, false},
{Policy{Enabled: true, NoInput: true}, Streams{InTTY: true, ErrTTY: true}, false},
{Policy{Enabled: true, Machine: true}, Streams{InTTY: true, ErrTTY: true}, false},
{Policy{Enabled: true, Machine: true, Interactive: true}, Streams{InTTY: true, ErrTTY: true}, true},
} {
require.Equal(t, tc.expected, tc.policy.CanPrompt(tc.streams))
}
prompt := &fakePrompt{}
value := "supplied"
asked, err := RequireInput(context.Background(), &value, "title", "title", nil, Streams{}, Policy{}, prompt)
require.NoError(t, err)
require.False(t, asked)
require.Zero(t, prompt.inputs)
value = ""
_, err = RequireInput(context.Background(), &value, "title", "title", nil, Streams{}, Policy{}, prompt)
require.ErrorContains(t, err, "--title")
}
// askingPrompt answers Asker prompts from scripted answers. Ask skips answers that validate rejects,
// as an inline re-prompt does, and cancels when none is left.
type askingPrompt struct {
fakePrompt
answers, rejected, titles []string
accept bool
defaults []bool
}
func (p *askingPrompt) Ask(_ context.Context, title string, validate func(string) error) (string, error) {
p.titles = append(p.titles, title)
for len(p.answers) > 0 {
answer := p.answers[0]
p.answers = p.answers[1:]
if validate == nil || validate(answer) == nil {
return answer, nil
}
p.rejected = append(p.rejected, answer)
}
return "", ErrCanceled
}
func (p *askingPrompt) AskText(_ context.Context, title, initial string) (string, error) {
p.titles = append(p.titles, title)
return initial + " edited", nil
}
func (p *askingPrompt) ConfirmDefault(_ context.Context, title string, defaultYes bool) (bool, error) {
p.titles = append(p.titles, title)
p.defaults = append(p.defaults, defaultYes)
return p.accept, nil
}
func TestConfirmAction(t *testing.T) {
terminal := Streams{InTTY: true, ErrTTY: true}
deleteLabel := Confirmation{Prompt: `Delete label "bug" from alice/x?`, NonTTY: `delete label "bug" from alice/x`}
typed := Confirmation{Prompt: "Delete alice/x?", Typed: "alice/x"}
for _, tc := range []struct {
name string
c Confirmation
yes bool
streams Streams
prompt Prompter
code int
message string
rejected []string
}{
{name: "accepted by flag", c: deleteLabel, yes: true, prompt: &fakePrompt{}},
{name: "names the action without a terminal", c: deleteLabel, prompt: &fakePrompt{}, code: clierr.Usage, message: `confirmation required to delete label "bug" from alice/x; rerun with --yes`},
{name: "without a terminal or action", c: Confirmation{Prompt: "Go?"}, prompt: &fakePrompt{}, code: clierr.Usage, message: "confirmation required; rerun with --yes"},
{name: "accepted", c: deleteLabel, streams: terminal, prompt: &askingPrompt{accept: true}},
{name: "declined", c: deleteLabel, streams: terminal, prompt: &askingPrompt{}, code: clierr.Canceled, message: "canceled"},
{name: "declined to a plain prompter", c: deleteLabel, streams: terminal, prompt: &fakePrompt{}, code: clierr.Canceled, message: "canceled"},
{name: "re-prompts until the name is typed", c: typed, streams: terminal, prompt: &askingPrompt{answers: []string{"alice/y", " Alice/X "}}, rejected: []string{"alice/y"}},
{name: "typed wrong to a plain prompter", c: typed, streams: terminal, prompt: &fakePrompt{}, code: clierr.Canceled, message: "canceled"},
} {
t.Run(tc.name, func(t *testing.T) {
err := ConfirmAction(context.Background(), tc.c, tc.yes, tc.streams, Policy{Enabled: true}, tc.prompt)
require.Equal(t, tc.code, clierr.ExitCode(err), "%v", err)
if tc.message != "" {
require.EqualError(t, err, tc.message)
}
if tc.code == clierr.Canceled {
require.ErrorIs(t, err, ErrCanceled)
}
if asker, ok := tc.prompt.(*askingPrompt); ok {
require.Equal(t, tc.rejected, asker.rejected)
}
})
}
}
func TestConfirmActionPassesTheDefault(t *testing.T) {
prompt := &askingPrompt{accept: true}
streams := Streams{InTTY: true, ErrTTY: true}
require.NoError(t, ConfirmAction(context.Background(), Confirmation{Prompt: "Create pull request?", DefaultYes: true}, false, streams, Policy{Enabled: true}, prompt))
require.NoError(t, ConfirmAction(context.Background(), Confirmation{Prompt: "Delete branch?"}, false, streams, Policy{Enabled: true}, prompt))
require.Equal(t, []bool{true, false}, prompt.defaults)
require.Equal(t, []string{"Create pull request?", "Delete branch?"}, prompt.titles)
}
func TestRequireInput(t *testing.T) {
streams, policy := Streams{InTTY: true, ErrTTY: true}, Policy{Enabled: true}
hex := func(s string) error {
if len(s) != 6 {
return errors.New("use six hex digits")
}
return nil
}
asker := &askingPrompt{answers: []string{"", "red", "d73a4a"}}
value := ""
asked, err := RequireInput(context.Background(), &value, "color", "Color (hex)", hex, streams, policy, asker)
require.NoError(t, err)
require.True(t, asked)
require.Equal(t, "d73a4a", value)
require.Equal(t, []string{"", "red"}, asker.rejected)
require.Equal(t, []string{"Color (hex)"}, asker.titles)
value = ""
_, err = RequireInput(context.Background(), &value, "color", "Color (hex)", hex, streams, policy, &fakePrompt{})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.EqualError(t, err, "--color: use six hex digits")
_, err = RequireInput(context.Background(), &value, "color", "Color (hex)", hex, Streams{}, policy, asker)
require.EqualError(t, err, "missing required input: supply --color")
}
func TestFormPrompterConfirmDefault(t *testing.T) {
for _, defaultYes := range []bool{true, false} {
p, _ := accessiblePrompter(t, "\n")
accepted, err := p.ConfirmDefault(context.Background(), "Submit?", defaultYes)
require.NoError(t, err)
require.Equal(t, defaultYes, accepted)
}
}
func TestFormPrompterAskRepromptsInline(t *testing.T) {
p, rendered := accessiblePrompter(t, "\n bug \n")
value, err := p.Ask(context.Background(), "Label name", func(s string) error {
if s == "" {
return errors.New("a value is required")
}
return nil
})
require.NoError(t, err)
require.Equal(t, "bug", value)
require.Contains(t, rendered.String(), "a value is required")
}
func TestPromptAbortIsCanceled(t *testing.T) {
err := promptError(context.Background(), huh.ErrUserAborted)
require.ErrorIs(t, err, ErrCanceled)
require.Equal(t, clierr.Canceled, clierr.ExitCode(err))
}