mirror of
https://github.com/peco/peco.git
synced 2026-09-10 07:16:29 -04:00
195 lines
5.8 KiB
Go
195 lines
5.8 KiB
Go
package peco
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/peco/peco/filter"
|
|
"github.com/peco/peco/hub"
|
|
"github.com/peco/peco/line"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// makeFollowBuffer builds a MemoryBuffer with n lines labelled "lineN".
|
|
func makeFollowBuffer(n int) *MemoryBuffer {
|
|
mb := NewMemoryBuffer(0)
|
|
for i := range n {
|
|
mb.lines = append(mb.lines, line.NewRaw(uint64(i), fmt.Sprintf("line%d", i), false, false))
|
|
}
|
|
return mb
|
|
}
|
|
|
|
// readScreenRow returns the text rendered on row y of the simulation screen,
|
|
// trailing blanks trimmed.
|
|
func readScreenRow(t *testing.T, s *SimScreen, y int) string {
|
|
t.Helper()
|
|
cells, w, _ := s.screen.GetContents()
|
|
var b strings.Builder
|
|
for x := range w {
|
|
c := cells[y*w+x]
|
|
if len(c.Runes) == 0 || c.Runes[0] == 0 {
|
|
b.WriteRune(' ')
|
|
continue
|
|
}
|
|
b.WriteRune(c.Runes[0])
|
|
}
|
|
return strings.TrimRight(b.String(), " ")
|
|
}
|
|
|
|
func newFollowState(t *testing.T, buf Buffer) (*Peco, *SimScreen) {
|
|
t.Helper()
|
|
screen := NewDummyScreen()
|
|
state := New()
|
|
state.screen = screen
|
|
state.Filters().Add(filter.NewIgnoreCase())
|
|
state.currentLineBuffer = buf
|
|
return state, screen
|
|
}
|
|
|
|
func TestWindowCrop(t *testing.T) {
|
|
buf := makeFollowBuffer(100)
|
|
|
|
t.Run("tail window", func(t *testing.T) {
|
|
fb := WindowCrop{offset: 90, perPage: 10}.Crop(buf)
|
|
require.Equal(t, 10, fb.Size())
|
|
first, err := fb.LineAt(0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "line90", first.DisplayString())
|
|
last, err := fb.LineAt(9)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "line99", last.DisplayString())
|
|
})
|
|
|
|
t.Run("partial window smaller than perPage", func(t *testing.T) {
|
|
small := makeFollowBuffer(5)
|
|
fb := WindowCrop{offset: 0, perPage: 10}.Crop(small)
|
|
require.Equal(t, 5, fb.Size())
|
|
})
|
|
|
|
t.Run("negative offset clamps to zero", func(t *testing.T) {
|
|
fb := WindowCrop{offset: -5, perPage: 3}.Crop(buf)
|
|
require.Equal(t, 3, fb.Size())
|
|
first, err := fb.LineAt(0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "line0", first.DisplayString())
|
|
})
|
|
|
|
t.Run("offset past end yields empty", func(t *testing.T) {
|
|
fb := WindowCrop{offset: 200, perPage: 10}.Crop(buf)
|
|
require.Equal(t, 0, fb.Size())
|
|
})
|
|
}
|
|
|
|
// TestGHIssue820_FollowKeepsNewestVisible verifies that with follow mode on,
|
|
// DrawScreen pins the viewport to the tail of the buffer so the newest line
|
|
// is rendered on the last visible row (like tail -f), regardless of page
|
|
// alignment.
|
|
func TestGHIssue820_FollowKeepsNewestVisible(t *testing.T) {
|
|
buf := makeFollowBuffer(50)
|
|
state, screen := newFollowState(t, buf)
|
|
state.Follow().Set(true)
|
|
|
|
// top-down-query-bottom is the layout from the issue: list anchored to
|
|
// the top (row 0), query at the bottom.
|
|
layout, err := TopDownQueryBottomLayout(state)
|
|
require.NoError(t, err)
|
|
|
|
layout.DrawScreen(state, nil)
|
|
|
|
loc := state.Location()
|
|
perPage := layout.linesPerPage()
|
|
|
|
require.Equal(t, 49, loc.LineNumber(), "cursor should be pinned to the newest line")
|
|
require.Equal(t, 50, loc.Total())
|
|
require.Equal(t, max(50-perPage, 0), loc.Offset(),
|
|
"offset should be a sliding window over the tail, not page-aligned")
|
|
|
|
// The list area is anchored at row 0; the newest line sits on the last
|
|
// visible row.
|
|
lastRow := perPage - 1
|
|
require.Equal(t, "line49", readScreenRow(t, screen, lastRow),
|
|
"newest line should be on the bottom row of the list area")
|
|
}
|
|
|
|
// TestFollowDefaultShowsHead verifies that without follow mode the viewport
|
|
// stays at the head of the buffer (unchanged behavior).
|
|
func TestFollowDefaultShowsHead(t *testing.T) {
|
|
buf := makeFollowBuffer(50)
|
|
state, screen := newFollowState(t, buf)
|
|
require.False(t, state.IsFollowing())
|
|
|
|
layout, err := TopDownQueryBottomLayout(state)
|
|
require.NoError(t, err)
|
|
|
|
layout.DrawScreen(state, nil)
|
|
|
|
loc := state.Location()
|
|
require.Equal(t, 0, loc.LineNumber())
|
|
require.Equal(t, 0, loc.Offset())
|
|
require.Equal(t, "line0", readScreenRow(t, screen, 0),
|
|
"oldest line should be on the top row when not following")
|
|
}
|
|
|
|
// TestFollowDisabledByManualScroll verifies that any manual vertical
|
|
// navigation turns follow mode off.
|
|
func TestFollowDisabledByManualScroll(t *testing.T) {
|
|
buf := makeFollowBuffer(50)
|
|
state, _ := newFollowState(t, buf)
|
|
state.Follow().Set(true)
|
|
|
|
layout, err := TopDownQueryBottomLayout(state)
|
|
require.NoError(t, err)
|
|
// Pin to the tail first.
|
|
layout.DrawScreen(state, nil)
|
|
require.True(t, state.IsFollowing())
|
|
|
|
moved := layout.MovePage(state, hub.ToLineAbove)
|
|
require.True(t, moved)
|
|
require.False(t, state.IsFollowing(),
|
|
"manual vertical scroll should disable follow mode")
|
|
}
|
|
|
|
// TestToggleFollowAction verifies the ToggleFollow action flips follow state.
|
|
func TestToggleFollowAction(t *testing.T) {
|
|
state := New()
|
|
state.hub = nullHub{}
|
|
require.False(t, state.IsFollowing())
|
|
|
|
doToggleFollow(t.Context(), state, Event{})
|
|
require.True(t, state.IsFollowing(), "follow should be on after first toggle")
|
|
|
|
doToggleFollow(t.Context(), state, Event{})
|
|
require.False(t, state.IsFollowing(), "follow should be off after second toggle")
|
|
}
|
|
|
|
// TestFollowConfigAndFlag verifies that --follow and the Follow config field
|
|
// both enable follow mode, with the CLI flag taking precedence.
|
|
func TestFollowConfigAndFlag(t *testing.T) {
|
|
t.Run("CLI flag", func(t *testing.T) {
|
|
p := newPeco()
|
|
require.NoError(t, p.ApplyConfig(CLIOptions{OptFollow: true}))
|
|
require.True(t, p.IsFollowing())
|
|
})
|
|
|
|
t.Run("config field", func(t *testing.T) {
|
|
p := newPeco()
|
|
p.config.Follow = true
|
|
require.NoError(t, p.ApplyConfig(CLIOptions{}))
|
|
require.True(t, p.IsFollowing())
|
|
})
|
|
|
|
t.Run("default off", func(t *testing.T) {
|
|
p := newPeco()
|
|
require.NoError(t, p.ApplyConfig(CLIOptions{}))
|
|
require.False(t, p.IsFollowing())
|
|
})
|
|
}
|
|
|
|
// TestToggleFollowRegistered verifies the action is wired into the registry so
|
|
// it can be bound from a keymap.
|
|
func TestToggleFollowRegistered(t *testing.T) {
|
|
_, ok := nameToActions["peco.ToggleFollow"]
|
|
require.True(t, ok, "peco.ToggleFollow should be registered")
|
|
}
|