v0.2.0 a558f49

readability_test.go RawHistoryFolder

1package selection
2
3import (
4	"strings"
5	"testing"
6
7	"github.com/bnema/nefercap/internal/core"
8	"github.com/bnema/nefercap/internal/ports"
9	"github.com/stretchr/testify/assert"
10)
11
12func TestContextualFooter(t *testing.T) {
13	m := newTestModel(t)
14	assert.Contains(t, m.footerText, "R region · M monitor")
15	assert.Contains(t, m.footerText, "G grid · Esc cancel")
16	assert.NotContains(t, m.footerText, "W workspace")
17	assert.Contains(t, m.footerText, "drag + release capture region")
18	m.input(key("m"))
19	assert.Contains(t, m.footerText, "Enter capture monitor")
20	m.input(key("r"))
21	assert.Contains(t, m.footerText, "drag + release capture region")
22
23	m, _ = newBareModel()
24	m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{Width: 640, Height: 480}}})
25	m.resize(640, 480, 1)
26	assert.Contains(t, m.footerText, "W workspace")
27	m.input(key("w"))
28	assert.Equal(t, core.PickWorkspace, m.picker.Kind())
29	assert.Contains(t, m.footerText, "Enter capture workspace")
30	m.resize(600, 400, 1)
31	assert.Contains(t, m.footerText, "drag + release capture region", "resize resets the mode")
32}
33
34func TestHintsBoundsAndMetrics(t *testing.T) {
35	for _, size := range [][2]int{{1920, 1080}, {640, 480}, {480, 320}, {200, 100}, {20, 20}} {
36		m := newTestModel(t)
37		m.resize(size[0], size[1], 1)
38		for _, r := range []frect{m.scene().header, m.scene().footer} {
39			assert.GreaterOrEqual(t, r.x, 0.0)
40			assert.GreaterOrEqual(t, r.y, 0.0)
41			assert.LessOrEqual(t, r.x+r.w, float64(size[0]))
42			assert.LessOrEqual(t, r.y+r.h, float64(size[1]))
43		}
44	}
45	assert.Contains(t, styleSheet, "font-size: 1rem")
46	assert.Contains(t, styleSheet, "justify-content: center")
47	assert.Contains(t, styleSheet, "align-items: center")
48	assert.Equal(t, 2, len(strings.Split(newTestModel(t).footerText, "\n")))
49	assert.GreaterOrEqual(t, footerH-2*tagPadY, 2*baseFont*1.37)
50}
51
52func TestNarrowWorkspaceFooterHoldsFourLines(t *testing.T) {
53	m, _ := newBareModel()
54	m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{Y: 60, Width: 480, Height: 260}}})
55	m.resize(480, 320, 1)
56	m.input(key("w"))
57	s := m.scene()
58	// Each explicit hint row wraps once at 480px: four 24px lines plus
59	// two 4px vertical padding edges. Do not derive this from the helper.
60	assert.GreaterOrEqual(t, s.footer.h, 104.0)
61	assert.LessOrEqual(t, s.footer.y+s.footer.h, 320.0)
62	if !raceEnabled {
63		assert.Zero(t, testing.AllocsPerRun(100, func() { _ = m.scene() }))
64	}
65}
66
67func TestWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
68	m, _ := newBareModel()
69	m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{Y: 60, Width: 640, Height: 360}}})
70	m.resize(640, 480, 1)
71	m.input(key("w"))
72	s := m.scene()
73	assert.Equal(t, "640 × 360", m.label.text)
74	assert.True(t, s.label.x+s.label.w <= s.header.x || s.label.x >= s.header.x+s.header.w ||
75		s.label.y+s.label.h <= s.header.y || s.label.y >= s.header.y+s.header.h, "label and header must not overlap")
76	assert.LessOrEqual(t, s.label.y+s.label.h, 480.0)
77}
78
79func TestRightWorkspaceLabelAvoidsOutputHeader(t *testing.T) {
80	m, _ := newBareModel()
81	m.setWorkspaces([]ports.Workspace{{ID: 11, OutputID: 7, Active: true, Region: ports.Region{X: 470, Y: 60, Width: 170, Height: 360}}})
82	m.resize(640, 480, 1)
83	m.input(key("w"))
84	s := m.scene()
85	assert.Greater(t, s.label.x+s.label.w, s.header.x, "label shares the header's horizontal range")
86	assert.Less(t, s.label.x, s.header.x+s.header.w)
87	assert.GreaterOrEqual(t, s.label.y, s.header.y+s.header.h, "collision moves dimensions below the header")
88	assert.GreaterOrEqual(t, s.label.y, 60.0, "dimensions move inside the workspace")
89}
90
91func TestOutputHeaderAtTopRight(t *testing.T) {
92	for _, width := range []int{1920, 640, 200, 20} {
93		m := newTestModel(t)
94		m.resize(width, 480, 1)
95		s := m.scene()
96		wantRightGap := min(margin, float64(width)-s.header.w)
97		assert.Equal(t, float64(width)-wantRightGap, s.header.x+s.header.w)
98		assert.GreaterOrEqual(t, s.header.x, 0.0)
99	}
100	m := newTestModel(t)
101	m.input(key("m"))
102	s := m.scene()
103	assert.Less(t, s.label.x+s.label.w, s.header.x, "monitor dimensions stay left of the output details")
104}
105
106func TestFooterUnchangedAllocations(t *testing.T) {
107	if raceEnabled {
108		t.Skip("the race detector adds allocations")
109	}
110	m := newTestModel(t)
111	m.input(key("m"))
112	assert.Zero(t, testing.AllocsPerRun(100, func() {
113		m.input(key("m"))
114		m.input(motion(10, 10))
115		_ = m.scene()
116	}))
117}

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 2

Tags 9

↑↓ moveenter openesc close