From ce7ab8bcf7e1f9b1099c33a2d3411716424c330a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Zi=C4=99bka?= Date: Tue, 13 Oct 2026 23:40:58 +0200 Subject: [PATCH] test(app): wait for the slow commands of two E2E tests - feedCmd skips a command that needs more than 200 ms, so that a test does not wait for a tea.Tick. Two tests need a command that can need more on Windows in CI. TestE2E_LoginFailure waits for a refused connection. TestE2E_CacheSurvivesRestart waits for the load of the home screen, which sends its requests and writes to the database file in one command. The two tests failed at times on Windows, and passed in the next run of the same commit. - feedCmdWithin is feedCmd with a wait of its own. The two tests use it with slowCmdWait (15 seconds) for these commands. feedCmd is as it was for each other test. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + internal/app/e2e_helpers_test.go | 23 ++++++++++++++++++++--- internal/app/e2e_test.go | 13 +++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index af7adc1..fafe92a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,6 +154,7 @@ Styles from theme config. Format helpers (timestamps, durations). `RunProgram` r - Unit tests per package - E2E tests in `internal/app/e2e_test.go` + `e2e_helpers_test.go` use mock HTTP server + mock player - E2E tests drive the bubbletea Update loop programmatically (no real TUI) +- `feedCmd` in `e2e_helpers_test.go` skips a command that needs more than 200 ms, so that a test does not wait for a `tea.Tick`. A request to an address with no server, or a load that writes to a database file, can need more on Windows in CI. For such a command, use `feedCmdWithin` with `slowCmdWait`. - `internal/app/model_test.go`, `playback_test.go`, `bookmarks_test.go`, `render_test.go` for app-level tests - `internal/ui/program_unix_test.go` starts the test program again as a child process, sends SIGHUP to it, and checks that it gets to the place where a `main` calls `Cleanup`. The child runs its program with `ui.RunProgram`, as each `main` does. It does not run on Windows. - `internal/cache/*_test.go` for cache store and helper behavior diff --git a/internal/app/e2e_helpers_test.go b/internal/app/e2e_helpers_test.go index 152fbd3..6a77916 100644 --- a/internal/app/e2e_helpers_test.go +++ b/internal/app/e2e_helpers_test.go @@ -515,10 +515,27 @@ func newE2EModelUnauthWithDB(t *testing.T, srv *httptest.Server, mp *mockPlayer) // E2E helpers — driving the Update→Cmd→Msg loop // --------------------------------------------------------------------------- +// feedCmdWait is the time that feedCmd waits for a command. +const feedCmdWait = 200 * time.Millisecond + +// slowCmdWait is the time that feedCmdWithin waits for a command that a test +// needs and that can be slow. It is longer than the timeout of one request of +// abs.Client, which is 10 seconds. +const slowCmdWait = 15 * time.Second + // feedCmd executes a tea.Cmd, feeds the resulting message back into Update, // and returns the new model + the cmd returned by Update. -// Commands that block longer than 200ms (e.g. tea.Tick) are skipped. +// Commands that block longer than feedCmdWait (e.g. tea.Tick) are skipped. func feedCmd(m Model, cmd tea.Cmd) (Model, tea.Cmd) { + return feedCmdWithin(m, cmd, feedCmdWait) +} + +// feedCmdWithin is feedCmd with a wait of its own. Use slowCmdWait for a +// command that the test must not skip and that can need more than feedCmdWait +// on a slow machine, for example on Windows in CI: a request to an address +// with no server, or a load that writes to a database file. A tea.Tick that +// is shorter than the wait is not skipped. +func feedCmdWithin(m Model, cmd tea.Cmd, wait time.Duration) (Model, tea.Cmd) { if cmd == nil { return m, nil } @@ -543,13 +560,13 @@ func feedCmd(m Model, cmd tea.Cmd) (Model, tea.Cmd) { if batch, ok := r.msg.(tea.BatchMsg); ok { var lastCmd tea.Cmd for _, c := range batch { - m, lastCmd = feedCmd(m, c) + m, lastCmd = feedCmdWithin(m, c, wait) } return m, lastCmd } res, nextCmd := m.Update(r.msg) return res.(Model), nextCmd - case <-time.After(200 * time.Millisecond): + case <-time.After(wait): // Drain the channel in case the goroutine completes after timeout go func() { <-ch; <-done }() return m, nil diff --git a/internal/app/e2e_test.go b/internal/app/e2e_test.go index fe66a86..c538ddf 100644 --- a/internal/app/e2e_test.go +++ b/internal/app/e2e_test.go @@ -103,8 +103,9 @@ func TestE2E_LoginFailure(t *testing.T) { // Press enter m, cmd := e2ePressSpecial(m, tea.KeyEnter) - // Execute login command — should fail (unreachable) - m, _ = feedCmd(m, cmd) + // Execute login command — should fail (unreachable). A refused connection + // can need more than feedCmdWait on Windows. + m, _ = feedCmdWithin(m, cmd, slowCmdWait) // Should still be on login with error assertScreen(t, m, ScreenLogin) @@ -2264,7 +2265,10 @@ func TestE2E_CacheSurvivesRestart(t *testing.T) { m1 := NewWithPlayer(cfg, store, cachedClient, cacheStore, mp) m1 = e2eSetSize(m1, 120, 40) - m1 = feedCmdChain(m1, m1.Init(), 5) + // The load of the home screen sends its requests and writes to the + // database file in one command, which can need more than feedCmdWait. + m1, cmd := feedCmdWithin(m1, m1.Init(), slowCmdWait) + m1 = feedCmdChain(m1, cmd, 4) if m1.home.Loading() { t.Fatal("home should not be loading after first init") @@ -2279,7 +2283,8 @@ func TestE2E_CacheSurvivesRestart(t *testing.T) { m2 := NewWithPlayer(cfg, store, cachedClient, cacheStore, mp) m2 = e2eSetSize(m2, 120, 40) - m2 = feedCmdChain(m2, m2.Init(), 5) + m2, cmd = feedCmdWithin(m2, m2.Init(), slowCmdWait) + m2 = feedCmdChain(m2, cmd, 4) if m2.home.Loading() { t.Fatal("home should not be loading after second init (cache should serve data)")