diff --git a/internal/cli/app.go b/internal/cli/app.go index c8a1fb4..fbed5d6 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -34,30 +34,44 @@ type App struct { // Run creates and runs the CLI application. func Run() { - app := &App{ //nolint:exhaustruct - 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 " + - "[white] to begin, " + - "or [yellow]/help[white] for commands", - ) - - err := app.ui.Run() + err := run(NewUI()) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) 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 " + + "[white] to begin, " + + "or [yellow]/help[white] for commands", + ) + }() + + return app.ui.Run() +} + func (a *App) handleInput(text string) { if strings.HasPrefix(text, "/") { a.handleCommand(text) diff --git a/internal/cli/app_internal_test.go b/internal/cli/app_internal_test.go index 3f133a5..6c5853b 100644 --- a/internal/cli/app_internal_test.go +++ b/internal/cli/app_internal_test.go @@ -4,6 +4,8 @@ import ( "io" "net/http" "net/http/httptest" + "slices" + "strings" "testing" "time" @@ -22,13 +24,22 @@ const ( ) // 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 // holdTime is how long TestLinesKeepOrder holds the first line, // which gives the second line time to start too early. 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) { t.Parallel() @@ -348,6 +359,78 @@ func TestLinesKeepOrder(t *testing.T) { 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 ") { + t.Errorf("second status line is %q, want the /connect hint", lines[1]) + } +} + // TestOwnEventFromOtherClient checks that a message, notice, // join or part that the user's session makes from another // client shows as one line in the buffer it belongs to.