okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
2ecf203
main
1// Where the detail goes is a pure rendering decision: the daemon sends the 2// same document at every size, so no e2e through the daemon can catch a 3// wrong split threshold, a pane that overflows the terminal, or detail 4// dropped from parsing. 5package tui 6 7import ( 8"strings" 9"testing" 10 11 tea"charm.land/bubbletea/v2" 12"github.com/charmbracelet/x/ansi" 13) 14 15func detailFixture ()* PageDocument { 16return & PageDocument { 17Title :"paperclips" , 18Summary :"1 open · 1 resolved" , 19Empty :"nothing vented yet" , 20Rows : []PageRow {{ 21ID :"1" , 22Text :"ruff not on PATH" , 23Badge :"open" , 24Tone :ToneWarning , 25Detail :"ruff is missing from PATH; the binary lives in pre-commit's env.\n\nsuggestion: document the canonical invocation" , 26 }, { 27ID :"2" , 28Text :"stale binary" , 29Badge :"resolved" , 30Tone :ToneMuted , 31 }}, 32 } 33} 34 35func pageAt (width ,height int ,doc * PageDocument )PageViewModel { 36m := NewPageViewModel (nil ,"s" ,"/paperclips" ) 37m .SetSize (width ,height ) 38m .setDoc (doc ) 39m .Busy = false 40return m 41} 42 43func TestPageFitsEveryTerminal (t * testing.T ) { 44for _ ,size := range [][2 ]int {{170 ,40 }, {120 ,30 }, {96 ,14 }, {80 ,24 }, {60 ,12 }, {40 ,8 }, {20 ,4 }} { 45view := pageAt (size [0 ],size [1 ],detailFixture ()).View () 46lines := strings .Split (view ,"\n" ) 47if len (lines )> size [1 ] { 48t .Fatalf ("%dx%d: %d lines:\n%s" ,size [0 ],size [1 ],len (lines ),view ) 49 } 50for _ ,line := range lines { 51if w := ansi .StringWidth (line );w > size [0 ] { 52t .Fatalf ("%dx%d: a line is %d wide:\n%s" ,size [0 ],size [1 ],w ,view ) 53 } 54 } 55 } 56} 57 58func TestWidePageShowsDetailBesideTheList (t * testing.T ) { 59view := ansi .Strip (pageAt (120 ,30 ,detailFixture ()).View ()) 60for _ ,want := range []string {"ruff not on PATH" ,"│" ,"suggestion: document" ,"open · #1" } { 61if ! strings .Contains (view ,want ) { 62t .Fatalf ("wide view lost %q:\n%s" ,want ,view ) 63 } 64 } 65} 66 67func TestNarrowPageStacksTheDetailUnderTheList (t * testing.T ) { 68view := ansi .Strip (pageAt (80 ,24 ,detailFixture ()).View ()) 69if strings .Contains (view ,"│" ) { 70t .Fatalf ("a narrow view should not split side by side:\n%s" ,view ) 71 } 72list ,detail := strings .Index (view ,"stale binary" ),strings .Index (view ,"pre-commit's env" ) 73if list < 0 || detail < list { 74t .Fatalf ("a narrow view should show the detail under the list:\n%s" ,view ) 75 } 76} 77 78func TestRowsWithoutDetailKeepTheFullWidth (t * testing.T ) { 79doc := detailFixture () 80doc .Rows [0 ].Detail = "" 81view := ansi .Strip (pageAt (120 ,30 ,doc ).View ()) 82if strings .Contains (view ,"│" )|| strings .Contains (view ,"open · #1" ) { 83t .Fatalf ("a page without details should keep the list alone:\n%s" ,view ) 84 } 85} 86 87// Actions are chords and never bare letters: a letter typed into a page 88// filters its rows, and only a chord asks for a question or runs one. E2E 89// cannot see which keys reach the filter, so these drive the model directly. 90func TestPageBareLetterFiltersInsteadOfActing (t * testing.T ) { 91doc := detailFixture () 92doc .Actions = []PageAction {{ID :"delete" ,Key :"ctrl+d" ,Label :"delete" ,Row :true ,Confirm :true ,Confirmation :"Delete this item?" }} 93m := pageAt (120 ,30 ,doc ) 94m ,_ = m .Update (tea.KeyPressMsg {Code :'d' ,Text :"d" }) 95if m .Mode != modeBrowse || m .CurrentAction != nil || m .listView .input .Value ()!= "d" { 96t .Fatalf ("a bare letter should only filter: mode %v, action %v, filter %q" ,m .Mode ,m .CurrentAction ,m .listView .input .Value ()) 97 } 98m ,_ = m .Update (tea.KeyPressMsg {Code :'d' ,Mod :tea .ModCtrl }) 99if m .Mode != modeConfirm || m .CurrentAction == nil || ! m .Confirming () { 100t .Fatal ("the chord should ask before deleting" ) 101 } 102if ! strings .Contains (ansi .Strip (m .View ()),"Delete this item?" ) { 103t .Fatalf ("the question should read in the footer:\n%s" ,ansi .Strip (m .View ())) 104 } 105m ,_ = m .Update (tea.KeyPressMsg {Code :'x' ,Text :"x" }) 106if m .Mode != modeConfirm || m .listView .input .Value ()!= "d" { 107t .Fatal ("a stray key should neither answer nor reach the filter while asking" ) 108 } 109m ,_ = m .Update (tea.KeyPressMsg {Code :tea .KeyEscape }) 110if m .Mode != modeBrowse || m .CurrentAction != nil || m .Confirming () { 111t .Fatal ("esc should drop the question" ) 112 } 113}