detached e6e645b

test(overview): cover side panels, partial bands with gaps and maximized cascade edges

commit
e6e645bfce2f2f0df49155ea351d6e8beafc0908
author
brice · · 2026-10-07 16:02 UTC
parent
9e14d33
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +145 -0
M internal/core/cascade_test.go +53 -0
@@ -3,6 +3,8 @@ package core
33 import (
44 "slices"
55 "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
68 )
79
810 func cascadeWorkspace(n int) *Workspace {
@@ -35,6 +37,57 @@ func TestCascadeBandWidthWithGaps(t *testing.T) {
3537 }
3638 }
3739
40+// A partial last band after a full one shares the band width with gaps:
41+// each band divides by its own column count.
42+func TestCascadePartialLastBandWithGaps(t *testing.T) {
43+ w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3}
44+ w.SetOutput(1000, 600)
45+ w.SetGaps(8)
46+ for id := WindowID(1); id <= 5; id++ {
47+ w.AddWindow(id)
48+ }
49+ third, half := (1000-4*8)/3, (1000-3*8)/2
50+ rects := w.columnRectsFor(false)
51+ for i, want := range []struct{ x, w int }{
52+ {8, third}, {8 + third + 8, third}, {8 + 2*(third+8), third},
53+ {8, half}, {8 + half + 8, half},
54+ } {
55+ if r := rects[i]; r.X != want.x || r.W != want.w {
56+ t.Fatalf("column %d rect %v, want X %d W %d", i, r, want.x, want.w)
57+ }
58+ }
59+ if rects[3].Y-rects[0].Y != 600 {
60+ t.Fatalf("band spacing %d, want 600", rects[3].Y-rects[0].Y)
61+ }
62+}
63+
64+// A side panel moves and narrows every band: cells start at the usable
65+// area's X and share its width.
66+func TestCascadeSidePanel(t *testing.T) {
67+ w := &Workspace{Overflow: OverflowCascade, MaxColumns: 2}
68+ w.SetOutput(1000, 600)
69+ w.SetUsable(Rect{X: 100, W: 900, H: 600})
70+ for id := WindowID(1); id <= 3; id++ {
71+ w.AddWindow(id)
72+ }
73+ rects := w.columnRectsFor(false)
74+ for i, want := range []Rect{
75+ {X: 100, Y: 0, W: 450, H: 600},
76+ {X: 550, Y: 0, W: 450, H: 600},
77+ {X: 100, Y: 600, W: 900, H: 600},
78+ } {
79+ if rects[i].X != want.X || rects[i].W != want.W || rects[i].Y-rects[0].Y != want.Y {
80+ t.Fatalf("column %d rect %v, want %v", i, rects[i], want)
81+ }
82+ }
83+ // The panel edge at x=100 is no shared line: column 1 has only its
84+ // right neighbor.
85+ w.FocusID(1)
86+ if p := previewOf(t, w.Layout(), 1); p.Neighbors != ports.SideRight {
87+ t.Fatalf("neighbors %04b, want right only", p.Neighbors)
88+ }
89+}
90+
3891 // Each band's columns share its width equally, as in fixed overflow: a
3992 // lone column fills it, a full band of three has thirds.
4093 func TestCascadeBandsAndReveal(t *testing.T) {
@@ -3,6 +3,8 @@ package core
3 import (3 import (
4 "slices"4 "slices"
5 "testing"5 "testing"
6+
7+ "github.com/bnema/neferwl/internal/ports"
6 )8 )
7 9
8 func cascadeWorkspace(n int) *Workspace {10 func cascadeWorkspace(n int) *Workspace {
@@ -35,6 +37,57 @@ func TestCascadeBandWidthWithGaps(t *testing.T) {
35 }37 }
36 }38 }
37 39
40+// A partial last band after a full one shares the band width with gaps:
41+// each band divides by its own column count.
42+func TestCascadePartialLastBandWithGaps(t *testing.T) {
43+ w := &Workspace{Overflow: OverflowCascade, MaxColumns: 3}
44+ w.SetOutput(1000, 600)
45+ w.SetGaps(8)
46+ for id := WindowID(1); id <= 5; id++ {
47+ w.AddWindow(id)
48+ }
49+ third, half := (1000-4*8)/3, (1000-3*8)/2
50+ rects := w.columnRectsFor(false)
51+ for i, want := range []struct{ x, w int }{
52+ {8, third}, {8 + third + 8, third}, {8 + 2*(third+8), third},
53+ {8, half}, {8 + half + 8, half},
54+ } {
55+ if r := rects[i]; r.X != want.x || r.W != want.w {
56+ t.Fatalf("column %d rect %v, want X %d W %d", i, r, want.x, want.w)
57+ }
58+ }
59+ if rects[3].Y-rects[0].Y != 600 {
60+ t.Fatalf("band spacing %d, want 600", rects[3].Y-rects[0].Y)
61+ }
62+}
63+
64+// A side panel moves and narrows every band: cells start at the usable
65+// area's X and share its width.
66+func TestCascadeSidePanel(t *testing.T) {
67+ w := &Workspace{Overflow: OverflowCascade, MaxColumns: 2}
68+ w.SetOutput(1000, 600)
69+ w.SetUsable(Rect{X: 100, W: 900, H: 600})
70+ for id := WindowID(1); id <= 3; id++ {
71+ w.AddWindow(id)
72+ }
73+ rects := w.columnRectsFor(false)
74+ for i, want := range []Rect{
75+ {X: 100, Y: 0, W: 450, H: 600},
76+ {X: 550, Y: 0, W: 450, H: 600},
77+ {X: 100, Y: 600, W: 900, H: 600},
78+ } {
79+ if rects[i].X != want.X || rects[i].W != want.W || rects[i].Y-rects[0].Y != want.Y {
80+ t.Fatalf("column %d rect %v, want %v", i, rects[i], want)
81+ }
82+ }
83+ // The panel edge at x=100 is no shared line: column 1 has only its
84+ // right neighbor.
85+ w.FocusID(1)
86+ if p := previewOf(t, w.Layout(), 1); p.Neighbors != ports.SideRight {
87+ t.Fatalf("neighbors %04b, want right only", p.Neighbors)
88+ }
89+}
90+
38 // Each band's columns share its width equally, as in fixed overflow: a91 // Each band's columns share its width equally, as in fixed overflow: a
39 // lone column fills it, a full band of three has thirds.92 // lone column fills it, a full band of three has thirds.
40 func TestCascadeBandsAndReveal(t *testing.T) {93 func TestCascadeBandsAndReveal(t *testing.T) {
M internal/core/overview_cascade_test.go +71 -0
@@ -678,6 +678,77 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) {
678678 }
679679 }
680680
681+// Left from the first column of a band whose maximized column hides it:
682+// the arrows and three fingers walk the cards, then enter the stash, or
683+// change workspace without one.
684+func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) {
685+ for _, stash := range []bool{false, true} {
686+ c, m := cascadeCore(t)
687+ w := m.Current()
688+ if stash {
689+ w.AddWindow(40)
690+ w.FocusID(40)
691+ w.ToggleWindowStash()
692+ }
693+ w.FocusID(3) // Band {2, 3}: 3 hides 2.
694+ w.ToggleFullWidth()
695+ m.ToggleOverview()
696+ swipe := func() {
697+ c.swipeBegin(ports.SwipeBegin{Fingers: 3})
698+ for range 4 {
699+ c.swipeUpdate(ports.SwipeUpdate{DX: -40})
700+ }
701+ c.swipeEnd(ports.SwipeEnd{})
702+ }
703+ swipe()
704+ if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth {
705+ t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget())
706+ }
707+ swipe()
708+ if stash {
709+ if m.Current() != w || m.cardAt(w) < 0 {
710+ t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w))
711+ }
712+ } else if m.Current() != m.Workspaces[0] {
713+ t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current()))
714+ }
715+ }
716+}
717+
718+// With four or more columns in a band, the cards three or more columns
719+// away sit under the two-step card: arrows still reach every one, in
720+// order, each taking the maximization.
721+func TestOverviewCascadeFarCardsByArrows(t *testing.T) {
722+ m := newMonitor("", "")
723+ m.SetOutput(1200, 600)
724+ m.SetMaxColumns(5)
725+ w := m.Current()
726+ w.Overflow = OverflowCascade
727+ for id := WindowID(1); id <= 5; id++ {
728+ m.AddWindow(id)
729+ }
730+ w.FocusID(1)
731+ w.ToggleFullWidth()
732+ m.ToggleOverview()
733+ ps := m.Layout()
734+ edge := previewOf(t, ps, 3).Rect
735+ for _, id := range []WindowID{4, 5} {
736+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X {
737+ t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X)
738+ }
739+ }
740+ for _, want := range []WindowID{2, 3, 4, 5} {
741+ m.OverviewMove(1, 0)
742+ if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth {
743+ t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want)
744+ }
745+ }
746+ m.CancelOverview()
747+ if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth {
748+ t.Fatalf("escape: focus %d, want maximized 1", focusedID(m))
749+ }
750+}
751+
681752 // Right from a stash card lands on the maximized column, which keeps its
682753 // maximization, not on the first card of its band.
683754 func TestOverviewCascadeStashToMaximized(t *testing.T) {
@@ -678,6 +678,77 @@ func TestOverviewCascadeThreeFingerMaximizedCards(t *testing.T) {
678 }678 }
679 }679 }
680 680
681+// Left from the first column of a band whose maximized column hides it:
682+// the arrows and three fingers walk the cards, then enter the stash, or
683+// change workspace without one.
684+func TestOverviewCascadeMaximizedLeftEdge(t *testing.T) {
685+ for _, stash := range []bool{false, true} {
686+ c, m := cascadeCore(t)
687+ w := m.Current()
688+ if stash {
689+ w.AddWindow(40)
690+ w.FocusID(40)
691+ w.ToggleWindowStash()
692+ }
693+ w.FocusID(3) // Band {2, 3}: 3 hides 2.
694+ w.ToggleFullWidth()
695+ m.ToggleOverview()
696+ swipe := func() {
697+ c.swipeBegin(ports.SwipeBegin{Fingers: 3})
698+ for range 4 {
699+ c.swipeUpdate(ports.SwipeUpdate{DX: -40})
700+ }
701+ c.swipeEnd(ports.SwipeEnd{})
702+ }
703+ swipe()
704+ if m.Current() != w || m.overviewTarget() != 2 || !w.Columns[w.Focus].FullWidth {
705+ t.Fatalf("stash %v: first swipe: target %d", stash, m.overviewTarget())
706+ }
707+ swipe()
708+ if stash {
709+ if m.Current() != w || m.cardAt(w) < 0 {
710+ t.Fatalf("past the band: workspace %d, card %d; want the stash", indexOf(m.Workspaces, m.Current()), m.cardAt(w))
711+ }
712+ } else if m.Current() != m.Workspaces[0] {
713+ t.Fatalf("past the band: workspace %d, want 0", indexOf(m.Workspaces, m.Current()))
714+ }
715+ }
716+}
717+
718+// With four or more columns in a band, the cards three or more columns
719+// away sit under the two-step card: arrows still reach every one, in
720+// order, each taking the maximization.
721+func TestOverviewCascadeFarCardsByArrows(t *testing.T) {
722+ m := newMonitor("", "")
723+ m.SetOutput(1200, 600)
724+ m.SetMaxColumns(5)
725+ w := m.Current()
726+ w.Overflow = OverflowCascade
727+ for id := WindowID(1); id <= 5; id++ {
728+ m.AddWindow(id)
729+ }
730+ w.FocusID(1)
731+ w.ToggleFullWidth()
732+ m.ToggleOverview()
733+ ps := m.Layout()
734+ edge := previewOf(t, ps, 3).Rect
735+ for _, id := range []WindowID{4, 5} {
736+ if p := previewOf(t, ps, id); p.Hidden || !p.Peek || p.Rect.X != edge.X {
737+ t.Fatalf("far card %d: %+v, want a card under 3 at x %d", id, p, edge.X)
738+ }
739+ }
740+ for _, want := range []WindowID{2, 3, 4, 5} {
741+ m.OverviewMove(1, 0)
742+ if m.Current() != w || m.overviewTarget() != want || !w.Columns[w.Focus].FullWidth {
743+ t.Fatalf("right: target %d, want maximized %d", m.overviewTarget(), want)
744+ }
745+ }
746+ m.CancelOverview()
747+ if focusedID(m) != 1 || !w.Columns[w.Focus].FullWidth {
748+ t.Fatalf("escape: focus %d, want maximized 1", focusedID(m))
749+ }
750+}
751+
681 // Right from a stash card lands on the maximized column, which keeps its752 // Right from a stash card lands on the maximized column, which keeps its
682 // maximization, not on the first card of its band.753 // maximization, not on the first card of its band.
683 func TestOverviewCascadeStashToMaximized(t *testing.T) {754 func TestOverviewCascadeStashToMaximized(t *testing.T) {
M internal/core/separators_test.go +21 -0
@@ -147,6 +147,27 @@ func TestSeparatorsScroll(t *testing.T) {
147147 }
148148 }
149149
150+// A scroll column scrolled under a side panel is no neighbor: the column
151+// next to it in the usable area gets no line on that side.
152+// With a gap, the edge the two share lies inside the usable area: only
153+// the tile under the panel being out of it keeps the line away.
154+func TestSeparatorsScrollUnderSidePanel(t *testing.T) {
155+ for _, gap := range []int{0, 8} {
156+ ps := []Placement{
157+ {ID: 1, Rect: Rect{X: 0, W: 100, H: 600}},
158+ {ID: 2, Rect: Rect{X: 100 + gap, W: 400, H: 600}, Focused: true},
159+ {ID: 3, Rect: Rect{X: 500 + 2*gap, W: 380, H: 600}},
160+ }
161+ setVisibleNeighbors(ps, gap, Rect{X: 100, W: 900, H: 600})
162+ if ps[0].Neighbors != 0 {
163+ t.Fatalf("gap %d: tile under the panel: neighbors %04b, want none", gap, ps[0].Neighbors)
164+ }
165+ if ps[1].Neighbors != ports.SideRight {
166+ t.Fatalf("gap %d: tile at the panel: neighbors %04b, want right only", gap, ps[1].Neighbors)
167+ }
168+ }
169+}
170+
150171 // A cascade band above a bottom panel: the next band starts under the
151172 // panel, so the focused tile gets no line along the panel, only the one it
152173 // shares with its band neighbor.
@@ -147,6 +147,27 @@ func TestSeparatorsScroll(t *testing.T) {
147 }147 }
148 }148 }
149 149
150+// A scroll column scrolled under a side panel is no neighbor: the column
151+// next to it in the usable area gets no line on that side.
152+// With a gap, the edge the two share lies inside the usable area: only
153+// the tile under the panel being out of it keeps the line away.
154+func TestSeparatorsScrollUnderSidePanel(t *testing.T) {
155+ for _, gap := range []int{0, 8} {
156+ ps := []Placement{
157+ {ID: 1, Rect: Rect{X: 0, W: 100, H: 600}},
158+ {ID: 2, Rect: Rect{X: 100 + gap, W: 400, H: 600}, Focused: true},
159+ {ID: 3, Rect: Rect{X: 500 + 2*gap, W: 380, H: 600}},
160+ }
161+ setVisibleNeighbors(ps, gap, Rect{X: 100, W: 900, H: 600})
162+ if ps[0].Neighbors != 0 {
163+ t.Fatalf("gap %d: tile under the panel: neighbors %04b, want none", gap, ps[0].Neighbors)
164+ }
165+ if ps[1].Neighbors != ports.SideRight {
166+ t.Fatalf("gap %d: tile at the panel: neighbors %04b, want right only", gap, ps[1].Neighbors)
167+ }
168+ }
169+}
170+
150 // A cascade band above a bottom panel: the next band starts under the171 // A cascade band above a bottom panel: the next band starts under the
151 // panel, so the focused tile gets no line along the panel, only the one it172 // panel, so the focused tile gets no line along the panel, only the one it
152 // shares with its band neighbor.173 // shares with its band neighbor.

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