diff --git a/internal/ui/screens/drain_test.go b/internal/ui/screens/drain_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6b01067f28c60e8878517f4efd221823d589907a --- /dev/null +++ b/internal/ui/screens/drain_test.go @@ -0,0 +1,31 @@ +package screens + +import ( + tea "charm.land/bubbletea/v2" + + "git.secluded.site/keld/internal/ui" +) + +// drain feeds commands back into a screen until a DoneMsg or BackMsg +// is produced, or the command chain is exhausted. This is necessary +// because huh uses internal message chains (nextFieldMsg → +// nextGroupMsg → StateCompleted) that must be processed sequentially. +func drain[S ui.Screen](s S, initialCmd tea.Cmd) (S, tea.Cmd) { + cmd := initialCmd + for cmd != nil { + msg := cmd() + if msg == nil { + return s, nil + } + switch msg.(type) { + case ui.DoneMsg: + return s, cmd + case ui.BackMsg: + return s, cmd + } + var screen ui.Screen + screen, cmd = s.Update(msg) + s = screen.(S) + } + return s, nil +} diff --git a/internal/ui/screens/filepicker_test.go b/internal/ui/screens/filepicker_test.go index 9d50b5ede9c89dff363cbc9da13b17c6dd245a59..6de5040bf21e212e7bd406d351ca114785df12d4 100644 --- a/internal/ui/screens/filepicker_test.go +++ b/internal/ui/screens/filepicker_test.go @@ -23,35 +23,13 @@ func testNodes() []restic.LsNode { } } -// drainFilePicker feeds commands back into the file picker screen -// until a DoneMsg or BackMsg is produced, or the chain is exhausted. -func drainFilePicker(fp *FilePicker, initialCmd tea.Cmd) (*FilePicker, tea.Cmd) { - cmd := initialCmd - for cmd != nil { - msg := cmd() - if msg == nil { - return fp, nil - } - switch msg.(type) { - case ui.DoneMsg: - return fp, cmd - case ui.BackMsg: - return fp, cmd - } - var screen ui.Screen - screen, cmd = fp.Update(msg) - fp = screen.(*FilePicker) - } - return fp, nil -} - // simulateFileLoad sends a filesLoadedMsg directly to the screen. func simulateFileLoad(fp *FilePicker, nodes []restic.LsNode, err error) *FilePicker { gen := atomic.LoadInt64(&fp.gen) msg := filesLoadedMsg{gen: gen, nodes: nodes, err: err} screen, cmd := fp.Update(msg) fp = screen.(*FilePicker) - fp, _ = drainFilePicker(fp, cmd) + fp, _ = drain(fp, cmd) return fp } @@ -304,7 +282,7 @@ func TestFilePickerIncludesWithPartialSelection(t *testing.T) { // (should be dir1, sorted dirs first) via space. screen, cmd := fp.Update(tea.KeyPressMsg{Code: ' ', Text: " "}) fp = screen.(*FilePicker) - fp, _ = drainFilePicker(fp, cmd) + fp, _ = drain(fp, cmd) // Confirm with enter. screen, cmd = fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) @@ -341,7 +319,7 @@ func TestFilePickerSelectionShowsCount(t *testing.T) { // Toggle first item and confirm. screen, cmd := fp.Update(tea.KeyPressMsg{Code: ' ', Text: " "}) fp = screen.(*FilePicker) - fp, _ = drainFilePicker(fp, cmd) + fp, _ = drain(fp, cmd) screen, _ = fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) fp = screen.(*FilePicker) diff --git a/internal/ui/screens/overwrite_test.go b/internal/ui/screens/overwrite_test.go index 653b50f944a28c2d148fc878c23a4e9c76dc6c93..8e008a9dc52bd215d29da532af84bd2a04799432 100644 --- a/internal/ui/screens/overwrite_test.go +++ b/internal/ui/screens/overwrite_test.go @@ -8,28 +8,6 @@ import ( "git.secluded.site/keld/internal/ui" ) -// drainOverwrite feeds commands back into the overwrite screen until -// a DoneMsg or BackMsg is produced, or the command chain is exhausted. -func drainOverwrite(o *Overwrite, initialCmd tea.Cmd) (*Overwrite, tea.Cmd) { - cmd := initialCmd - for cmd != nil { - msg := cmd() - if msg == nil { - return o, nil - } - switch msg.(type) { - case ui.DoneMsg: - return o, cmd - case ui.BackMsg: - return o, cmd - } - var screen ui.Screen - screen, cmd = o.Update(msg) - o = screen.(*Overwrite) - } - return o, nil -} - func TestOverwriteTitle(t *testing.T) { t.Parallel() @@ -88,7 +66,7 @@ func TestOverwriteCompleteReturnsDone(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = o.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainOverwrite(screen.(*Overwrite), cmd) + _, cmd = drain(screen.(*Overwrite), cmd) if cmd == nil { t.Fatal("expected DoneCmd after form completion") @@ -110,7 +88,7 @@ func TestOverwriteDefaultIsIfChanged(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = o.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - o, _ = drainOverwrite(screen.(*Overwrite), cmd) + o, _ = drain(screen.(*Overwrite), cmd) if got := o.Value(); got != "if-changed" { t.Errorf("Value() = %q, want %q (default should be if-changed)", got, "if-changed") @@ -128,7 +106,7 @@ func TestOverwriteSelectionShowsValue(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = o.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - o, _ = drainOverwrite(screen.(*Overwrite), cmd) + o, _ = drain(screen.(*Overwrite), cmd) if got := o.Selection(); got != "if-changed" { t.Errorf("Selection() = %q, want %q", got, "if-changed") @@ -146,7 +124,7 @@ func TestOverwriteReinitAfterBack(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = o.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - o, _ = drainOverwrite(screen.(*Overwrite), cmd) + o, _ = drain(screen.(*Overwrite), cmd) // Re-init (simulates back navigation). o.Init() @@ -154,7 +132,7 @@ func TestOverwriteReinitAfterBack(t *testing.T) { // Should be functional again. screen, cmd = o.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainOverwrite(screen.(*Overwrite), cmd) + _, cmd = drain(screen.(*Overwrite), cmd) if cmd == nil { t.Fatal("expected DoneCmd after re-init") diff --git a/internal/ui/screens/preset_test.go b/internal/ui/screens/preset_test.go index ee69673bfdcfe0813db97b7ac56973ce8b1ce550..fac0d20f38bd3e7cfff6f2e77f9f9e010665da18 100644 --- a/internal/ui/screens/preset_test.go +++ b/internal/ui/screens/preset_test.go @@ -14,33 +14,6 @@ func testPresets() []string { return []string{"home@cloud", "work@local", "media"} } -// drainPreset feeds commands back into the preset screen until a -// DoneMsg or BackMsg is produced, or the command chain is exhausted. -// This is necessary because huh uses internal message chains -// (nextFieldMsg → nextGroupMsg → StateCompleted) that must be -// processed sequentially. -func drainPreset(p *Preset, initialCmd tea.Cmd) (*Preset, tea.Cmd) { - cmd := initialCmd - for cmd != nil { - msg := cmd() - if msg == nil { - return p, nil - } - // Check if this is one of our terminal messages. - switch msg.(type) { - case ui.DoneMsg: - return p, cmd - case ui.BackMsg: - return p, cmd - } - // Otherwise feed it back to the screen. - var screen ui.Screen - screen, cmd = p.Update(msg) - p = screen.(*Preset) - } - return p, nil -} - func TestPresetTitle(t *testing.T) { t.Parallel() @@ -132,7 +105,7 @@ func TestPresetCompleteReturnsDone(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainPreset(screen.(*Preset), cmd) + _, cmd = drain(screen.(*Preset), cmd) if cmd == nil { t.Fatal("expected DoneCmd after form completion") @@ -155,7 +128,7 @@ func TestPresetSelectionValue(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) if got := p.Selection(); got != "home@cloud" { t.Errorf("Selection() = %q, want %q", got, "home@cloud") @@ -175,7 +148,7 @@ func TestPresetGlobalDefaultDisplayLabel(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) // Selection() should return a display-friendly label, not "". sel := p.Selection() @@ -196,7 +169,7 @@ func TestPresetReinitPreservesSelection(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) if p.Selection() != "home@cloud" { t.Fatalf("precondition: Selection() = %q, want %q", p.Selection(), "home@cloud") @@ -223,7 +196,7 @@ func TestPresetReinitAfterBack(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) // Re-initialise (simulates the session navigating back to this screen). p.Init() @@ -231,7 +204,7 @@ func TestPresetReinitAfterBack(t *testing.T) { // Should be functional again — selecting should produce DoneCmd. screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainPreset(screen.(*Preset), cmd) + _, cmd = drain(screen.(*Preset), cmd) if cmd == nil { t.Fatal("expected DoneCmd after re-init, form may not have been reset") @@ -276,7 +249,7 @@ func TestPresetValueReturnsActualPresetName(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) if got := p.Value(); got != "home@cloud" { t.Errorf("Value() = %q, want %q", got, "home@cloud") @@ -294,7 +267,7 @@ func TestPresetValueEmptyForGlobalDefaults(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = p.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - p, _ = drainPreset(screen.(*Preset), cmd) + p, _ = drain(screen.(*Preset), cmd) if got := p.Value(); got != "" { t.Errorf("Value() = %q, want empty for global defaults", got) diff --git a/internal/ui/screens/snapshot_test.go b/internal/ui/screens/snapshot_test.go index babe79a542898d9dc17bebbe38cb86f22688eb4a..225f0f2961a090d783ee98c52c7b4b8a846bc417 100644 --- a/internal/ui/screens/snapshot_test.go +++ b/internal/ui/screens/snapshot_test.go @@ -32,28 +32,6 @@ func testSnapshots() []restic.Snapshot { } } -// drainSnapshot feeds commands back into the snapshot screen until a -// DoneMsg or BackMsg is produced, or the command chain is exhausted. -func drainSnapshot(s *Snapshot, initialCmd tea.Cmd) (*Snapshot, tea.Cmd) { - cmd := initialCmd - for cmd != nil { - msg := cmd() - if msg == nil { - return s, nil - } - switch msg.(type) { - case ui.DoneMsg: - return s, cmd - case ui.BackMsg: - return s, cmd - } - var screen ui.Screen - screen, cmd = s.Update(msg) - s = screen.(*Snapshot) - } - return s, nil -} - // simulateLoad sends a snapshotsLoadedMsg directly to the screen, // bypassing tea.Batch. This is how tests deliver async results — // in the real runtime, the Batch command produces this message. @@ -63,7 +41,7 @@ func simulateLoad(s *Snapshot, snaps []restic.Snapshot, err error) *Snapshot { screen, cmd := s.Update(msg) s = screen.(*Snapshot) // Drain any form init commands. - s, _ = drainSnapshot(s, cmd) + s, _ = drain(s, cmd) return s } @@ -120,7 +98,7 @@ func TestSnapshotLoadsAndPresentsSelect(t *testing.T) { // Should now be in the selecting phase. Press enter to pick first. screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, cmd = drainSnapshot(screen.(*Snapshot), cmd) + s, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after selecting a snapshot") @@ -146,7 +124,7 @@ func TestSnapshotSelectionShowsShortID(t *testing.T) { // Select first snapshot. screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, _ = drainSnapshot(screen.(*Snapshot), cmd) + s, _ = drain(screen.(*Snapshot), cmd) if got := s.Selection(); got != "abcdef01" { t.Errorf("Selection() = %q, want %q", got, "abcdef01") @@ -175,7 +153,7 @@ func TestSnapshotErrorFallsBackToManual(t *testing.T) { } screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, cmd = drainSnapshot(screen.(*Snapshot), cmd) + s, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after manual entry") @@ -209,7 +187,7 @@ func TestSnapshotNoRepoFallsBackSilently(t *testing.T) { s = screen.(*Snapshot) } screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, cmd = drainSnapshot(screen.(*Snapshot), cmd) + s, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after manual entry") @@ -243,7 +221,7 @@ func TestSnapshotNilLoaderGoesDirectToManual(t *testing.T) { s = screen.(*Snapshot) // Drain any init commands from the manual form. - s, _ = drainSnapshot(s, cmd) + s, _ = drain(s, cmd) // With no loader, should start in manual entry immediately. for _, ch := range "latest" { @@ -251,7 +229,7 @@ func TestSnapshotNilLoaderGoesDirectToManual(t *testing.T) { s = screen.(*Snapshot) } screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, cmd = drainSnapshot(screen.(*Snapshot), cmd) + s, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after manual entry") @@ -280,7 +258,7 @@ func TestSnapshotManualEntryFromSelect(t *testing.T) { // Select the manual entry option. screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, _ = drainSnapshot(screen.(*Snapshot), cmd) + s, _ = drain(screen.(*Snapshot), cmd) // Should now be in manual entry phase. Type an ID. for _, ch := range "abc123:subfolder" { @@ -288,7 +266,7 @@ func TestSnapshotManualEntryFromSelect(t *testing.T) { s = screen.(*Snapshot) } screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - s, cmd = drainSnapshot(screen.(*Snapshot), cmd) + s, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after manual entry from select") @@ -364,7 +342,7 @@ func TestSnapshotStaleLoadIgnored(t *testing.T) { // Now should be in selecting phase. screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainSnapshot(screen.(*Snapshot), cmd) + _, cmd = drain(screen.(*Snapshot), cmd) if cmd == nil { t.Fatal("expected DoneCmd after selecting from fresh load") diff --git a/internal/ui/screens/target_test.go b/internal/ui/screens/target_test.go index f9546cd2b9887a2f36d2a39fad08aaf8850a1884..8989ed32cf1980c8a5a5d8eb9754cac01fe01b37 100644 --- a/internal/ui/screens/target_test.go +++ b/internal/ui/screens/target_test.go @@ -8,31 +8,6 @@ import ( "git.secluded.site/keld/internal/ui" ) -// drainTarget feeds commands back into the target screen until a -// DoneMsg or BackMsg is produced, or the command chain is exhausted. -// This is necessary because huh uses internal message chains -// (nextFieldMsg → nextGroupMsg → StateCompleted) that must be -// processed sequentially. -func drainTarget(t *Target, initialCmd tea.Cmd) (*Target, tea.Cmd) { - cmd := initialCmd - for cmd != nil { - msg := cmd() - if msg == nil { - return t, nil - } - switch msg.(type) { - case ui.DoneMsg: - return t, cmd - case ui.BackMsg: - return t, cmd - } - var screen ui.Screen - screen, cmd = t.Update(msg) - t = screen.(*Target) - } - return t, nil -} - func TestTargetTitle(t *testing.T) { t.Parallel() @@ -95,7 +70,7 @@ func TestTargetCompleteReturnsDone(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainTarget(screen.(*Target), cmd) + _, cmd = drain(screen.(*Target), cmd) if cmd == nil { t.Fatal("expected DoneCmd after form completion") @@ -120,7 +95,7 @@ func TestTargetSelectionShowsPath(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - tgt, _ = drainTarget(screen.(*Target), cmd) + tgt, _ = drain(screen.(*Target), cmd) if got := tgt.Selection(); got != "/tmp/restore" { t.Errorf("Selection() = %q, want %q", got, "/tmp/restore") @@ -141,7 +116,7 @@ func TestTargetValueReturnsPath(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - tgt, _ = drainTarget(screen.(*Target), cmd) + tgt, _ = drain(screen.(*Target), cmd) if got := tgt.Value(); got != "/tmp/restore" { t.Errorf("Value() = %q, want %q", got, "/tmp/restore") @@ -162,7 +137,7 @@ func TestTargetReinitAfterBack(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - tgt, _ = drainTarget(screen.(*Target), cmd) + tgt, _ = drain(screen.(*Target), cmd) // Re-init (simulates back navigation). tgt.Init() @@ -173,7 +148,7 @@ func TestTargetReinitAfterBack(t *testing.T) { tgt.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)}) } screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - _, cmd = drainTarget(screen.(*Target), cmd) + _, cmd = drain(screen.(*Target), cmd) if cmd == nil { t.Fatal("expected DoneCmd after re-init") @@ -197,7 +172,7 @@ func TestTargetPreservesValueOnReinit(t *testing.T) { var screen ui.Screen var cmd tea.Cmd screen, cmd = tgt.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) - tgt, _ = drainTarget(screen.(*Target), cmd) + tgt, _ = drain(screen.(*Target), cmd) // Re-init should preserve the entered value. tgt.Init()