Show the neoirc-cli screen without waiting at start-up (closes #226)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Run added the start-up status bar and welcome lines through SetStatus and AddStatus before it started the tview event loop. Both wait for that loop, so neoirc-cli stopped before it showed its screen. They now run on a goroutine, as typed lines do. Run now passes a new UI to run, which holds the start-up path and returns its error. TestStartUp calls run with a simulation screen. It fails unless, within five seconds, the status window holds two lines: first the one with "Welcome to neoirc-cli", then the /connect hint. Model: opus-5-5
This commit was merged in pull request #230.
This commit is contained in:
+32
-18
@@ -34,30 +34,44 @@ type App struct {
|
|||||||
|
|
||||||
// Run creates and runs the CLI application.
|
// Run creates and runs the CLI application.
|
||||||
func Run() {
|
func Run() {
|
||||||
app := &App{ //nolint:exhaustruct
|
err := run(NewUI())
|
||||||
ui: NewUI(),
|
|
||||||
nick: "guest",
|
|
||||||
}
|
|
||||||
|
|
||||||
app.ui.OnInput(app.handleInput)
|
|
||||||
app.ui.SetStatus(app.nick, "", "disconnected")
|
|
||||||
|
|
||||||
app.ui.AddStatus(
|
|
||||||
"Welcome to neoirc-cli — an IRC-style client",
|
|
||||||
)
|
|
||||||
app.ui.AddStatus(
|
|
||||||
"Type [yellow]/connect <server-url>" +
|
|
||||||
"[white] to begin, " +
|
|
||||||
"or [yellow]/help[white] for commands",
|
|
||||||
)
|
|
||||||
|
|
||||||
err := app.ui.Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run starts the client on ui and returns when ui stops. Tests
|
||||||
|
// call it with ui on a simulation screen.
|
||||||
|
func run(ui *UI) error {
|
||||||
|
app := &App{ //nolint:exhaustruct
|
||||||
|
ui: ui,
|
||||||
|
nick: "guest",
|
||||||
|
}
|
||||||
|
|
||||||
|
app.ui.OnInput(app.handleInput)
|
||||||
|
|
||||||
|
// SetStatus and AddStatus wait for the event loop, which
|
||||||
|
// app.ui.Run starts below, so they run on a goroutine. The
|
||||||
|
// nick is read first, as a typed /nick may change app.nick.
|
||||||
|
nick := app.nick
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
app.ui.SetStatus(nick, "", "disconnected")
|
||||||
|
|
||||||
|
app.ui.AddStatus(
|
||||||
|
"Welcome to neoirc-cli — an IRC-style client",
|
||||||
|
)
|
||||||
|
app.ui.AddStatus(
|
||||||
|
"Type [yellow]/connect <server-url>" +
|
||||||
|
"[white] to begin, " +
|
||||||
|
"or [yellow]/help[white] for commands",
|
||||||
|
)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return app.ui.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) handleInput(text string) {
|
func (a *App) handleInput(text string) {
|
||||||
if strings.HasPrefix(text, "/") {
|
if strings.HasPrefix(text, "/") {
|
||||||
a.handleCommand(text)
|
a.handleCommand(text)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -22,13 +24,22 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// eventLoopTimeout is how long a test waits for a typed line to
|
// eventLoopTimeout is how long a test waits for a typed line to
|
||||||
// be handled, or for the UI to stop, before it fails.
|
// be handled, for the welcome lines, or for the UI to stop, before
|
||||||
|
// it fails.
|
||||||
const eventLoopTimeout = 5 * time.Second
|
const eventLoopTimeout = 5 * time.Second
|
||||||
|
|
||||||
// holdTime is how long TestLinesKeepOrder holds the first line,
|
// holdTime is how long TestLinesKeepOrder holds the first line,
|
||||||
// which gives the second line time to start too early.
|
// which gives the second line time to start too early.
|
||||||
const holdTime = 200 * time.Millisecond
|
const holdTime = 200 * time.Millisecond
|
||||||
|
|
||||||
|
// welcomeLines is the number of lines run adds to the status
|
||||||
|
// window when the client starts.
|
||||||
|
const welcomeLines = 2
|
||||||
|
|
||||||
|
// checkInterval is how often TestStartUp reads the status window
|
||||||
|
// while it waits for the welcome lines.
|
||||||
|
const checkInterval = 10 * time.Millisecond
|
||||||
|
|
||||||
func TestMemberPrefix(t *testing.T) {
|
func TestMemberPrefix(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -348,6 +359,78 @@ func TestLinesKeepOrder(t *testing.T) {
|
|||||||
waitHandled(t, handled, secondLine)
|
waitHandled(t, handled, secondLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestStartUp runs the client's start-up path on a simulation
|
||||||
|
// screen. It fails unless the event loop starts and the status
|
||||||
|
// window gets two lines: first the one that holds "Welcome to
|
||||||
|
// neoirc-cli", then the /connect hint.
|
||||||
|
func TestStartUp(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ui := NewUI()
|
||||||
|
ui.app.SetScreen(tcell.NewSimulationScreen(""))
|
||||||
|
|
||||||
|
// One slot, so that run's goroutine can end even after the
|
||||||
|
// cleanup below has stopped waiting for it.
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
go func() { done <- run(ui) }()
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ui.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("run: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(eventLoopTimeout):
|
||||||
|
t.Error("run did not return after the UI was stopped")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// run adds the welcome lines on a goroutine, so the status
|
||||||
|
// window is read until it holds two lines. Each read waits for
|
||||||
|
// the event loop, so the reads run on a goroutine too, which
|
||||||
|
// this test stops waiting for after eventLoopTimeout. The
|
||||||
|
// channel has one slot, so that goroutine can still end.
|
||||||
|
shown := make(chan []string, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
ui.app.QueueUpdate(func() {
|
||||||
|
lines = slices.Clone(ui.getOrCreateBuffer("(status)").Lines)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(lines) == welcomeLines {
|
||||||
|
shown <- lines
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(checkInterval)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
select {
|
||||||
|
case lines = <-shown:
|
||||||
|
case <-time.After(eventLoopTimeout):
|
||||||
|
t.Fatalf("the status window did not get the welcome lines within %v",
|
||||||
|
eventLoopTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(lines[0], "Welcome to neoirc-cli") {
|
||||||
|
t.Errorf("first status line is %q, want the welcome line", lines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(lines[1], "/connect <server-url>") {
|
||||||
|
t.Errorf("second status line is %q, want the /connect hint", lines[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestOwnEventFromOtherClient checks that a message, notice,
|
// TestOwnEventFromOtherClient checks that a message, notice,
|
||||||
// join or part that the user's session makes from another
|
// join or part that the user's session makes from another
|
||||||
// client shows as one line in the buffer it belongs to.
|
// client shows as one line in the buffer it belongs to.
|
||||||
|
|||||||
Reference in New Issue
Block a user