feat/hints a471fa4

step_clock_test.go RawHistoryFolder

1package core_test
2
3import (
4	"sync"
5	"testing"
6	"time"
7
8	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9	"github.com/bnema/neferwl/internal/ports"
10	"github.com/stretchr/testify/mock"
11)
12
13// stepClock is a clock the test moves: springs sample it, so a test lands
14// them by advancing it and sending page flips, never by sleeping. Its
15// timers never fire: frames come from the test.
16type stepClock struct {
17	mu    sync.Mutex
18	now   time.Time
19	clock ports.Clock
20}
21
22func newStepClock(t *testing.T) *stepClock {
23	t.Helper()
24	s := &stepClock{now: time.Unix(100, 0)}
25	clock := portsmocks.NewMockClock(t)
26	clock.EXPECT().Now().RunAndReturn(func() time.Time {
27		s.mu.Lock()
28		defer s.mu.Unlock()
29		return s.now
30	}).Maybe()
31	// One timer built before core runs: a mock created by core while the
32	// test ends would race its own cleanup check.
33	timer := portsmocks.NewMockTimer(t)
34	timer.EXPECT().C().Return(make(chan time.Time)).Maybe()
35	timer.EXPECT().Stop().Return(true).Maybe()
36	timer.EXPECT().Reset(mock.Anything).Return(false).Maybe()
37	clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe()
38	s.clock = clock
39	return s
40}
41
42func (s *stepClock) advance(d time.Duration) {
43	s.mu.Lock()
44	s.now = s.now.Add(d)
45	s.mu.Unlock()
46}
47
48// flip moves the clock by d and sends a page flip for each output, then
49// returns the scene set core published for them (ok false: none, so no
50// spring ran). Core publishes before it takes its next event, so a flip that
51// names no output is a barrier: once its send returns, every earlier event
52// is handled and the scene channel holds the result. One is sent first too,
53// and a scene left over from earlier events is dropped, so a scene found
54// afterwards is the flips' own.
55func (s *stepClock) flip(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, d time.Duration, outputs ...string) ([]ports.Scene, bool) {
56	t.Helper()
57	send(frames, ports.OutputFrame{Output: "sync"})
58	select {
59	case <-scenes:
60	default:
61	}
62	s.advance(d)
63	for _, o := range outputs {
64		send(frames, ports.OutputFrame{Output: o})
65	}
66	send(frames, ports.OutputFrame{Output: "sync"})
67	select {
68	case set := <-scenes:
69		return set, true
70	default:
71		return nil, false
72	}
73}
74
75// settle is flip past every spring.
76func (s *stepClock) settle(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, outputs ...string) ([]ports.Scene, bool) {
77	t.Helper()
78	return s.flip(t, frames, scenes, 5*time.Second, outputs...)
79}
80
81// landRig is a multiRig whose settle lands every running spring: with
82// animations on it runs on a clock the test moves (swipeRig) and sends the
83// page flips; with animations off it is the plain rig and land is the
84// identity. Each test body runs once per mode, so the off variant keeps
85// its meaning and the on variant asserts the same settled facts.
86type landRig struct {
87	*multiRig
88	land func([]ports.Scene) []ports.Scene
89	// lands counts the settles that found a spring running.
90	lands *int
91}
92
93// startLanding runs core on the given outputs (gaps and terminal policy as
94// in startMulti) with animations on or off.
95func startLanding(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *landRig {
96	t.Helper()
97	if !animated {
98		r := startMulti(t, func(c *ports.Config) {
99			if edit != nil {
100				edit(c)
101			}
102			c.Animations.On = false
103		}, outs...)
104		return &landRig{multiRig: r, land: func(set []ports.Scene) []ports.Scene { return set }, lands: new(int)}
105	}
106	defaults := altCmdDefaults()
107	r := startSwipeOn(t, func(c *ports.Config) {
108		c.Layout.Gaps, c.Terminal.AutoOpen = defaults.Layout.Gaps, defaults.Terminal.AutoOpen
109		if edit != nil {
110			edit(c)
111		}
112		c.Animations.On = true
113	}, outs...)
114	lands := new(int)
115	// An on variant that never saw a spring would be the off variant again.
116	t.Cleanup(func() {
117		if *lands == 0 {
118			t.Error("no spring ever ran: the animations-on variant checked nothing")
119		}
120	})
121	return &landRig{multiRig: r.multiRig, lands: lands, land: func(set []ports.Scene) []ports.Scene {
122		landed, ok := r.settleAll(t, r.outs...)
123		if !ok {
124			return set
125		}
126		*lands++
127		return landed
128	}}
129}
130
131// both runs body with animations off, then on. A rig of the on run fails the
132// test when it never settled a running spring (startLanding,
133// startAnimatedDragRig).
134func both(t *testing.T, body func(t *testing.T, animated bool)) {
135	t.Helper()
136	t.Run("off", func(t *testing.T) { body(t, false) })
137	t.Run("on", func(t *testing.T) { body(t, true) })
138}
139
140// keyLanded presses a key and returns the settled scene set.
141func (r *landRig) keyLanded(t *testing.T, sym string, mods ports.Mods) []ports.Scene {
142	t.Helper()
143	return r.land(r.key(t, sym, mods))
144}
145
146// mapLanded maps a window and returns the settled scene set.
147func (r *landRig) mapLanded(t *testing.T, id ports.WindowID) []ports.Scene {
148	t.Helper()
149	return r.land(r.mapWindow(t, id))
150}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close