@@ -3,6 +3,8 @@ package render
33import (
44 "bytes"
55 "math/rand/v2"
6+ "path/filepath"
7+ "regexp"
68 "strings"
79 "testing"
810 "time"
@@ -12,10 +14,16 @@ import (
1214 tea "github.com/charmbracelet/bubbletea"
1315)
1416
17+ var skylineANSIPattern = regexp .MustCompile (`\x1b\[[0-9;]*m` )
18+
1519func resetSkylineRNG () {
1620 rng = rand .New (rand .NewPCG (42 , 0 ))
1721}
1822
23+ func stripSkylineANSI (s string ) string {
24+ return skylineANSIPattern .ReplaceAllString (s , "" )
25+ }
26+
1927func TestSkylineFilterCodeFiles (t * testing.T ) {
2028 tests := []struct {
2129 name string
@@ -209,6 +217,26 @@ func TestSkylineRenderStaticIncludesTitleAndStats(t *testing.T) {
209217 }
210218}
211219
220+ func TestSkylineUsesRootBaseNameWhenNameMissing (t * testing.T ) {
221+ resetSkylineRNG ()
222+
223+ root := t .TempDir ()
224+ project := scanner.Project {
225+ Root : root ,
226+ Files : []scanner.FileInfo {
227+ {Path : "src/main.go" , Ext : ".go" , Size : 256 },
228+ },
229+ }
230+
231+ var buf bytes.Buffer
232+ Skyline (& buf , project , true )
233+
234+ out := stripSkylineANSI (buf .String ())
235+ if ! strings .Contains (out , "─── " + filepath .Base (root )+ " ───" ) {
236+ t .Fatalf ("expected skyline title to use root basename, got:\n %s" , out )
237+ }
238+ }
239+
212240func TestSkylineAnimationModelUpdateAndView (t * testing.T ) {
213241 resetSkylineRNG ()
214242
@@ -250,6 +278,122 @@ func TestSkylineAnimationModelUpdateAndView(t *testing.T) {
250278 }
251279}
252280
281+ func TestAnimationModelInitAndPhaseTransitions (t * testing.T ) {
282+ resetSkylineRNG ()
283+
284+ m := animationModel {
285+ arranged : []building {{height : 3 , char : '▓' , color : Cyan , extLabel : ".go" , gap : 1 }},
286+ width : 20 ,
287+ leftMargin : 2 ,
288+ sceneLeft : 1 ,
289+ sceneRight : 12 ,
290+ sceneWidth : 11 ,
291+ maxBuildingHeight : 3 ,
292+ phase : 1 ,
293+ visibleRows : 5 ,
294+ }
295+
296+ if cmd := m .Init (); cmd == nil {
297+ t .Fatal ("expected Init to return a tick command" )
298+ }
299+
300+ updated , cmd := m .Update (tickMsg (time .Now ()))
301+ if cmd == nil {
302+ t .Fatal ("expected tick command during rising phase" )
303+ }
304+
305+ m1 := updated .(animationModel )
306+ if m1 .phase != 2 {
307+ t .Fatalf ("expected phase transition to 2, got %d" , m1 .phase )
308+ }
309+ if m1 .frame != 0 {
310+ t .Fatalf ("expected frame reset after phase transition, got %d" , m1 .frame )
311+ }
312+
313+ m1 .frame = 39
314+ updated , cmd = m1 .Update (tickMsg (time .Now ()))
315+ if cmd == nil {
316+ t .Fatal ("expected quit command when animation completes" )
317+ }
318+
319+ m2 := updated .(animationModel )
320+ if ! m2 .done {
321+ t .Fatal ("expected animation model to be marked done" )
322+ }
323+ }
324+
325+ func TestAnimationModelUpdateShootingStarLifecycle (t * testing.T ) {
326+ resetSkylineRNG ()
327+
328+ m := animationModel {
329+ arranged : []building {{height : 4 , char : '▓' , color : Cyan , extLabel : ".go" , gap : 1 }},
330+ width : 20 ,
331+ leftMargin : 2 ,
332+ sceneLeft : 3 ,
333+ sceneRight : 10 ,
334+ sceneWidth : 7 ,
335+ maxBuildingHeight : 4 ,
336+ phase : 2 ,
337+ frame : 9 ,
338+ shootingStarActive : false ,
339+ }
340+
341+ updated , cmd := m .Update (tickMsg (time .Now ()))
342+ if cmd == nil {
343+ t .Fatal ("expected tick command in twinkling phase" )
344+ }
345+
346+ m1 := updated .(animationModel )
347+ if ! m1 .shootingStarActive {
348+ t .Fatal ("expected shooting star to activate on frame 10" )
349+ }
350+ if m1 .shootingStarCol != m .sceneLeft {
351+ t .Fatalf ("expected shooting star to start at scene left %d, got %d" , m .sceneLeft , m1 .shootingStarCol )
352+ }
353+
354+ m1 .shootingStarCol = m1 .sceneRight + 1
355+ updated , cmd = m1 .Update (tickMsg (time .Now ()))
356+ if cmd == nil {
357+ t .Fatal ("expected tick command when advancing active shooting star" )
358+ }
359+
360+ m2 := updated .(animationModel )
361+ if m2 .shootingStarActive {
362+ t .Fatal ("expected shooting star to deactivate after leaving the scene" )
363+ }
364+ }
365+
366+ func TestAnimationModelViewRendersLabelsAndShootingStar (t * testing.T ) {
367+ resetSkylineRNG ()
368+
369+ m := animationModel {
370+ arranged : []building {
371+ {height : 4 , char : '▓' , color : Cyan , extLabel : ".go" , gap : 1 },
372+ {height : 4 , char : '▒' , color : Yellow , extLabel : "A-1" , gap : 1 },
373+ },
374+ width : 24 ,
375+ leftMargin : 2 ,
376+ sceneLeft : 1 ,
377+ sceneRight : 20 ,
378+ sceneWidth : 19 ,
379+ starPositions : [][2 ]int {{0 , 2 }},
380+ moonCol : 12 ,
381+ maxBuildingHeight : 4 ,
382+ phase : 2 ,
383+ visibleRows : 6 ,
384+ shootingStarActive : true ,
385+ shootingStarRow : 0 ,
386+ shootingStarCol : 4 ,
387+ }
388+
389+ out := stripSkylineANSI (m .View ())
390+ for _ , want := range []string {".go" , "A-1" , "★" , "◐" , "▀" } {
391+ if ! strings .Contains (out , want ) {
392+ t .Fatalf ("expected view to contain %q, got:\n %s" , want , out )
393+ }
394+ }
395+ }
396+
253397func TestSkylineMinMax (t * testing.T ) {
254398 tests := []struct {
255399 name string
0 commit comments