11package redesign_test
22
33import (
4- "bytes"
5- "os"
64 "path/filepath"
75 "strings"
86 "testing"
9- "time "
7+ "testing/synctest "
108
119 tea "charm.land/bubbletea/v2"
12- "github.com/charmbracelet/colorprofile"
1310
1411 "github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/redesign"
1512 "github.com/gruntwork-io/terragrunt/pkg/options"
@@ -19,50 +16,6 @@ import (
1916 "github.com/stretchr/testify/require"
2017)
2118
22- // runModel starts a tea.Program with the given model, sends messages via
23- // the interact callback, and returns the final model once the program exits.
24- func runModel (t * testing.T , m redesign.Model , width , height int , interact func (p * tea.Program )) redesign.Model {
25- t .Helper ()
26-
27- var out bytes.Buffer
28-
29- pr , pw , err := os .Pipe ()
30- require .NoError (t , err )
31-
32- defer pr .Close ()
33- defer pw .Close ()
34-
35- p := tea .NewProgram (m ,
36- tea .WithInput (pr ),
37- tea .WithOutput (& out ),
38- tea .WithWindowSize (width , height ),
39- tea .WithColorProfile (colorprofile .TrueColor ),
40- )
41-
42- done := make (chan tea.Model , 1 )
43-
44- go func () {
45- finalModel , err := p .Run ()
46- assert .NoError (t , err )
47-
48- done <- finalModel
49- }()
50-
51- time .Sleep (50 * time .Millisecond )
52-
53- interact (p )
54-
55- select {
56- case fm := <- done :
57- return fm .(redesign.Model )
58- case <- time .After (10 * time .Second ):
59- p .Kill ()
60- t .Fatal ("program did not exit within timeout" )
61-
62- return redesign.Model {}
63- }
64- }
65-
6619// makeComponents builds a deterministic list of ComponentEntry values for
6720// testing. Each entry has a distinct Dir so Title() returns the directory
6821// basename and sort order is predictable.
@@ -90,38 +43,37 @@ func makeComponents(t *testing.T) []*redesign.ComponentEntry {
9043func TestModelStreamingInsertsSorted (t * testing.T ) {
9144 t .Parallel ()
9245
93- opts , err := options .NewTerragruntOptionsForTest ("" )
94- require .NoError (t , err )
46+ synctest .Test (t , func (t * testing.T ) {
47+ opts , err := options .NewTerragruntOptionsForTest ("" )
48+ require .NoError (t , err )
9549
96- l := logger .CreateLogger ()
97- components := makeComponents (t )
98- require .GreaterOrEqual (t , len (components ), 2 , "need at least 2 components" )
50+ l := logger .CreateLogger ()
51+ components := makeComponents (t )
52+ require .GreaterOrEqual (t , len (components ), 2 , "need at least 2 components" )
9953
100- // Start with the last component alphabetically
101- componentCh := make ( chan * redesign.ComponentEntry , len (components ))
102- m := redesign . NewModelStreaming ( l , opts , components [ len ( components ) - 1 ], componentCh )
54+ componentCh := make ( chan * redesign. ComponentEntry , len ( components ))
55+ m := redesign .NewModelStreaming ( l , opts , components [ len (components )- 1 ], componentCh )
56+ close ( componentCh )
10357
104- finalModel := runModel (t , m , 120 , 40 , func (p * tea.Program ) {
105- // Send the remaining components in reverse order
58+ msgs := make ([]tea.Msg , 0 , len (components ))
10659 for i := len (components ) - 2 ; i >= 0 ; i -- {
107- p .Send (redesign .ComponentMsg (components [i ]))
108- time .Sleep (50 * time .Millisecond )
60+ msgs = append (msgs , redesign .ComponentMsg (components [i ]))
10961 }
11062
111- time . Sleep ( 100 * time . Millisecond )
63+ msgs = append ( msgs , tea. KeyPressMsg { Code : 'q' , Text : "q" } )
11264
113- p .Send (tea.KeyPressMsg {Code : 'q' , Text : "q" })
114- })
65+ finalModel := driveModel (t , m , 120 , 40 , msgs ).(redesign.Model )
11566
116- assert .Equal (t , redesign .ListState , finalModel .State )
117- items := finalModel .List ().Items ()
118- assert .Len (t , items , len (components ), "all components should be in the list" )
67+ assert .Equal (t , redesign .ListState , finalModel .State )
68+ items := finalModel .List ().Items ()
69+ assert .Len (t , items , len (components ), "all components should be in the list" )
11970
120- for i := 1 ; i < len (items ); i ++ {
121- prev := strings .ToLower (items [i - 1 ].(* redesign.ComponentEntry ).Title ())
122- curr := strings .ToLower (items [i ].(* redesign.ComponentEntry ).Title ())
123- assert .LessOrEqual (t , prev , curr , "components should be in alphabetical order: %q should come before %q" , prev , curr )
124- }
71+ for i := 1 ; i < len (items ); i ++ {
72+ prev := strings .ToLower (items [i - 1 ].(* redesign.ComponentEntry ).Title ())
73+ curr := strings .ToLower (items [i ].(* redesign.ComponentEntry ).Title ())
74+ assert .LessOrEqual (t , prev , curr , "components should be in alphabetical order: %q should come before %q" , prev , curr )
75+ }
76+ })
12577}
12678
12779// makeMixedComponents returns a module entry followed by a template entry
@@ -150,131 +102,132 @@ func makeMixedComponents(t *testing.T) []*redesign.ComponentEntry {
150102func TestModelTabsFilterByKind (t * testing.T ) {
151103 t .Parallel ()
152104
153- opts , err := options .NewTerragruntOptionsForTest ("" )
154- require .NoError (t , err )
105+ synctest .Test (t , func (t * testing.T ) {
106+ opts , err := options .NewTerragruntOptionsForTest ("" )
107+ require .NoError (t , err )
155108
156- l := logger .CreateLogger ()
157- components := makeMixedComponents (t )
109+ l := logger .CreateLogger ()
110+ components := makeMixedComponents (t )
158111
159- componentCh := make (chan * redesign.ComponentEntry , len (components ))
160- m := redesign .NewModelStreaming (l , opts , components [0 ], componentCh )
161-
162- finalModel := runModel (t , m , 120 , 40 , func (p * tea.Program ) {
163- p .Send (redesign .ComponentMsg (components [1 ]))
164- time .Sleep (100 * time .Millisecond )
112+ componentCh := make (chan * redesign.ComponentEntry , len (components ))
113+ m := redesign .NewModelStreaming (l , opts , components [0 ], componentCh )
114+ close (componentCh )
165115
166116 // Cycle: All -> Templates (first tab after All in the current order).
167- p .Send (tea.KeyPressMsg {Code : tea .KeyTab })
168- time .Sleep (30 * time .Millisecond )
117+ msgs := []tea.Msg {
118+ redesign .ComponentMsg (components [1 ]),
119+ tea.KeyPressMsg {Code : tea .KeyTab },
120+ tea.KeyPressMsg {Code : 'q' , Text : "q" },
121+ }
169122
170- p .Send (tea.KeyPressMsg {Code : 'q' , Text : "q" })
171- })
123+ finalModel := driveModel (t , m , 120 , 40 , msgs ).(redesign.Model )
172124
173- assert .Equal (t , redesign .TabTemplates , finalModel .ActiveTab (), "tab key should cycle to Templates" )
125+ assert .Equal (t , redesign .TabTemplates , finalModel .ActiveTab (), "tab key should cycle to Templates" )
174126
175- templatesItems := finalModel .List ().Items ()
176- require .Len (t , templatesItems , 1 , "Templates tab should contain only the one template" )
177- assert .Equal (t , redesign .ComponentKindTemplate , templatesItems [0 ].(* redesign.ComponentEntry ).Kind ())
127+ templatesItems := finalModel .List ().Items ()
128+ require .Len (t , templatesItems , 1 , "Templates tab should contain only the one template" )
129+ assert .Equal (t , redesign .ComponentKindTemplate , templatesItems [0 ].(* redesign.ComponentEntry ).Kind ())
130+ })
178131}
179132
180133// TestModelTabShiftTabCycles verifies that shift+tab cycles tabs in
181134// reverse order.
182135func TestModelTabShiftTabCycles (t * testing.T ) {
183136 t .Parallel ()
184137
185- opts , err := options .NewTerragruntOptionsForTest ("" )
186- require .NoError (t , err )
138+ synctest .Test (t , func (t * testing.T ) {
139+ opts , err := options .NewTerragruntOptionsForTest ("" )
140+ require .NoError (t , err )
187141
188- l := logger .CreateLogger ()
189- components := makeMixedComponents (t )
142+ l := logger .CreateLogger ()
143+ components := makeMixedComponents (t )
190144
191- componentCh := make (chan * redesign.ComponentEntry , len (components ))
192- m := redesign .NewModelStreaming (l , opts , components [0 ], componentCh )
193-
194- finalModel := runModel (t , m , 120 , 40 , func (p * tea.Program ) {
195- p .Send (redesign .ComponentMsg (components [1 ]))
196- time .Sleep (100 * time .Millisecond )
145+ componentCh := make (chan * redesign.ComponentEntry , len (components ))
146+ m := redesign .NewModelStreaming (l , opts , components [0 ], componentCh )
147+ close (componentCh )
197148
198149 // Starts on All. Shift+Tab wraps to the last tab (Stacks).
199- p .Send (tea.KeyPressMsg {Code : tea .KeyTab , Mod : tea .ModShift })
200- time .Sleep (30 * time .Millisecond )
150+ msgs := []tea.Msg {
151+ redesign .ComponentMsg (components [1 ]),
152+ tea.KeyPressMsg {Code : tea .KeyTab , Mod : tea .ModShift },
153+ tea.KeyPressMsg {Code : 'q' , Text : "q" },
154+ }
201155
202- p .Send (tea.KeyPressMsg {Code : 'q' , Text : "q" })
203- })
156+ finalModel := driveModel (t , m , 120 , 40 , msgs ).(redesign.Model )
204157
205- assert .Equal (t , redesign .TabModules , finalModel .ActiveTab (), "shift+tab from All should wrap to the last tab" )
158+ assert .Equal (t , redesign .TabModules , finalModel .ActiveTab (), "shift+tab from All should wrap to the last tab" )
159+ })
206160}
207161
208- // TestModelCopyActionMaterializesUnit drives the Model through a real
209- // scaffold-key press on a unit component and asserts that the copy action
210- // runs end-to-end: the unit's files land in the working directory and a
211- // terragrunt.values.hcl stub is generated from the referenced values.*.
212- func TestModelCopyActionMaterializesUnit (t * testing.T ) {
162+ // TestModelCopyActionTransitionsToScaffoldState asserts that pressing the
163+ // scaffold key on a copyable component transitions the Model to
164+ // ScaffoldState, which is what dispatches the copy action.
165+ //
166+ // The copy itself is exercised end-to-end in copy_test.go; here we only
167+ // verify the wire-up, because tea.Exec (used by the copy dispatch) only runs
168+ // the underlying ExecCommand inside a real bubbletea runtime.
169+ func TestModelCopyActionTransitionsToScaffoldState (t * testing.T ) {
213170 t .Parallel ()
214171
215- repoDir := helpers .TmpDirWOSymlinks (t )
172+ synctest .Test (t , func (t * testing.T ) {
173+ repoDir := helpers .TmpDirWOSymlinks (t )
216174
217- unitBody := `locals { region = values.region }` + "\n "
218- writeFile (t , filepath .Join (repoDir , "vpc" , "terragrunt.hcl" ), unitBody )
175+ unitBody := `locals { region = values.region }` + "\n "
176+ writeFile (t , filepath .Join (repoDir , "vpc" , "terragrunt.hcl" ), unitBody )
219177
220- repo := newFakeRepo (t , repoDir )
178+ repo := newFakeRepo (t , repoDir )
221179
222- components , err := redesign .NewComponentDiscovery ().Discover (repo )
223- require .NoError (t , err )
224- require .Len (t , components , 1 )
225- require .Equal (t , redesign .ComponentKindUnit , components [0 ].Kind )
180+ components , err := redesign .NewComponentDiscovery ().Discover (repo )
181+ require .NoError (t , err )
182+ require .Len (t , components , 1 )
183+ require .Equal (t , redesign .ComponentKindUnit , components [0 ].Kind )
226184
227- workingDir := t .TempDir ()
185+ opts , err := options .NewTerragruntOptionsForTest ("" )
186+ require .NoError (t , err )
228187
229- opts , err := options .NewTerragruntOptionsForTest ("" )
230- require .NoError (t , err )
188+ opts .WorkingDir = t .TempDir ()
231189
232- opts . WorkingDir = workingDir
190+ entry := redesign . NewComponentEntry ( components [ 0 ]). WithSource ( "github.com/gruntwork-io/fake-repo" )
233191
234- entry := redesign .NewComponentEntry (components [0 ]).WithSource ("github.com/gruntwork-io/fake-repo" )
192+ componentCh := make (chan * redesign.ComponentEntry )
193+ close (componentCh )
235194
236- componentCh := make (chan * redesign.ComponentEntry )
237- m := redesign .NewModelStreaming (logger .CreateLogger (), opts , entry , componentCh )
238-
239- finalModel := runModel (t , m , 120 , 40 , func (p * tea.Program ) {
240- // 's' is the scaffold key. For units and stacks, this dispatches to
241- // the copy action via primaryActionCmd.
242- p .Send (tea.KeyPressMsg {Code : 's' , Text : "s" })
243- })
195+ m := redesign .NewModelStreaming (logger .CreateLogger (), opts , entry , componentCh )
244196
245- assert .FileExists (t , filepath .Join (workingDir , "terragrunt.hcl" ))
246- assert .FileExists (t , filepath .Join (workingDir , "terragrunt.values.hcl" ))
197+ msgs := []tea.Msg {tea.KeyPressMsg {Code : 's' , Text : "s" }}
247198
248- raw , err := os .ReadFile (filepath .Join (workingDir , "terragrunt.values.hcl" ))
249- require .NoError (t , err )
250- assert .Contains (t , string (raw ), "region" )
199+ finalModel := driveModel (t , m , 120 , 40 , msgs ).(redesign.Model )
251200
252- assert .Contains (t , finalModel .ExitMessage (), "terragrunt.values.hcl generated" ,
253- "the model should stash a post-exit callout after a successful copy" )
201+ assert .Equal (t , redesign .ScaffoldState , finalModel .State ,
202+ "pressing 's' on a unit should transition to ScaffoldState" )
203+ })
254204}
255205
256206// TestModelStreamingDeduplicates verifies that sending the same component
257207// twice does not result in a duplicate entry in the list.
258208func TestModelStreamingDeduplicates (t * testing.T ) {
259209 t .Parallel ()
260210
261- opts , err := options .NewTerragruntOptionsForTest ("" )
262- require .NoError (t , err )
211+ synctest .Test (t , func (t * testing.T ) {
212+ opts , err := options .NewTerragruntOptionsForTest ("" )
213+ require .NoError (t , err )
214+
215+ l := logger .CreateLogger ()
216+ components := makeComponents (t )
217+ require .NotEmpty (t , components )
263218
264- l := logger . CreateLogger ( )
265- components := makeComponents ( t )
266- require . NotEmpty ( t , components )
219+ componentCh := make ( chan * redesign. ComponentEntry , len ( components ) )
220+ m := redesign . NewModelStreaming ( l , opts , components [ 0 ], componentCh )
221+ close ( componentCh )
267222
268- componentCh := make (chan * redesign.ComponentEntry , len (components ))
269- m := redesign .NewModelStreaming (l , opts , components [0 ], componentCh )
223+ msgs := []tea.Msg {
224+ redesign .ComponentMsg (components [0 ]),
225+ tea.KeyPressMsg {Code : 'q' , Text : "q" },
226+ }
270227
271- finalModel := runModel (t , m , 120 , 40 , func (p * tea.Program ) {
272- p .Send (redesign .ComponentMsg (components [0 ]))
273- time .Sleep (100 * time .Millisecond )
228+ finalModel := driveModel (t , m , 120 , 40 , msgs ).(redesign.Model )
274229
275- p .Send (tea.KeyPressMsg {Code : 'q' , Text : "q" })
230+ assert .Equal (t , redesign .ListState , finalModel .State )
231+ assert .Len (t , finalModel .List ().Items (), 1 , "duplicate component should not appear twice" )
276232 })
277-
278- assert .Equal (t , redesign .ListState , finalModel .State )
279- assert .Len (t , finalModel .List ().Items (), 1 , "duplicate component should not appear twice" )
280233}
0 commit comments