mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
176 lines
6.7 KiB
Go
176 lines
6.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 }
|
|
|
|
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))
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
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) SecretInput(context.Context, string) (string, error) { return "", nil }
|
|
|
|
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 *askingPrompt
|
|
code int
|
|
message string
|
|
rejected []string
|
|
defaults []bool
|
|
}{
|
|
{name: "accepted by flag", c: deleteLabel, yes: true, prompt: &askingPrompt{}},
|
|
{name: "names the action without a terminal", c: deleteLabel, prompt: &askingPrompt{}, code: clierr.Usage, message: `confirmation required to delete label "bug" from alice/x; rerun with --yes`},
|
|
{name: "names the prompted action without a terminal", c: Confirmation{Prompt: "Delete alice/x? It becomes read-only."}, prompt: &askingPrompt{}, code: clierr.Usage, message: "confirmation required to delete alice/x; rerun with --yes"},
|
|
{name: "without a terminal or action", c: Confirmation{Prompt: "Go?", DefaultYes: true}, prompt: &askingPrompt{}, code: clierr.Usage, message: "confirmation required; rerun with --yes"},
|
|
{name: "accepted", c: deleteLabel, streams: terminal, prompt: &askingPrompt{accept: true}, defaults: []bool{false}},
|
|
{name: "passes the default", c: Confirmation{Prompt: "Create pull request?", DefaultYes: true}, streams: terminal, prompt: &askingPrompt{accept: true}, defaults: []bool{true}},
|
|
{name: "declined", c: deleteLabel, streams: terminal, prompt: &askingPrompt{}, code: clierr.Canceled, message: "canceled", defaults: []bool{false}},
|
|
{name: "re-prompts until the name is typed", c: typed, streams: terminal, prompt: &askingPrompt{answers: []string{"alice/y", " Alice/X "}}, rejected: []string{"alice/y"}},
|
|
} {
|
|
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)
|
|
}
|
|
require.Equal(t, tc.rejected, tc.prompt.rejected)
|
|
require.Equal(t, tc.defaults, tc.prompt.defaults)
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
asked, err = RequireInput(context.Background(), &value, "color", "Color (hex)", hex, Streams{}, policy, asker)
|
|
require.NoError(t, err, "a supplied value needs no prompt")
|
|
require.False(t, asked)
|
|
value = ""
|
|
_, 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))
|
|
}
|