11import { LoadingBar } from './LoadingBar.js'
2+ import { Stdout } from '../../ui.js'
23import { render } from '../../testing/ui.js'
34import { shouldDisplayColors , unstyled } from '../../../../public/node/output.js'
45import useLayout from '../hooks/use-layout.js'
@@ -25,196 +26,188 @@ beforeEach(() => {
2526 vi . mocked ( shouldDisplayColors ) . mockReturnValue ( true )
2627} )
2728
29+ /**
30+ * Creates a Stdout test double simulating a TTY stream (the default for
31+ * interactive terminals). On real Node streams, `isTTY` is only defined
32+ * as an own property when the stream IS a TTY — it's absent otherwise.
33+ */
34+ function createTTYStdout ( columns = 100 ) {
35+ const stdout = new Stdout ( { columns} ) as Stdout & { isTTY : boolean }
36+ stdout . isTTY = true
37+ return stdout
38+ }
39+
40+ /**
41+ * Creates a Stdout test double simulating a non-TTY environment
42+ * (piped output, CI without a pseudo-TTY, AI coding agents).
43+ */
44+ function createNonTTYStdout ( columns = 100 ) {
45+ const stdout = new Stdout ( { columns} ) as Stdout & { isTTY : boolean }
46+ stdout . isTTY = false
47+ return stdout
48+ }
49+
50+ /**
51+ * Renders LoadingBar with a TTY stdout and returns the last frame.
52+ * Most tests need a TTY to verify the animated progress bar renders.
53+ */
54+ function renderWithTTY ( element : React . ReactElement ) {
55+ const stdout = createTTYStdout ( )
56+ const instance = render ( element , { stdout} )
57+ return { lastFrame : stdout . lastFrame , unmount : instance . unmount , stdout}
58+ }
59+
2860describe ( 'LoadingBar' , ( ) => {
2961 test ( 'renders loading bar with default colored characters' , async ( ) => {
30- // Given
31- const title = 'Loading content'
32-
33- // When
34- const { lastFrame} = render ( < LoadingBar title = { title } /> )
62+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Loading content" /> )
3563
36- // Then
3764 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
3865 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
3966 Loading content ..."
4067 ` )
4168 } )
4269
4370 test ( 'renders loading bar with hill pattern when noColor prop is true' , async ( ) => {
44- // Given
45- const title = 'Processing files'
71+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Processing files" noColor /> )
4672
47- // When
48- const { lastFrame} = render ( < LoadingBar title = { title } noColor /> )
49-
50- // Then
5173 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
5274 "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
5375 Processing files ..."
5476 ` )
5577 } )
5678
5779 test ( 'renders loading bar with hill pattern when shouldDisplayColors returns false' , async ( ) => {
58- // Given
5980 vi . mocked ( shouldDisplayColors ) . mockReturnValue ( false )
60- const title = 'Downloading packages'
6181
62- // When
63- const { lastFrame} = render ( < LoadingBar title = { title } /> )
82+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Downloading packages" /> )
6483
65- // Then
6684 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
6785 "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
6886 Downloading packages ..."
6987 ` )
7088 } )
7189
7290 test ( 'handles narrow terminal width correctly' , async ( ) => {
73- // Given
74- vi . mocked ( useLayout ) . mockReturnValue ( {
75- twoThirds : 20 ,
76- oneThird : 10 ,
77- fullWidth : 30 ,
78- } )
79- const title = 'Building app'
80-
81- // When
82- const { lastFrame} = render ( < LoadingBar title = { title } /> )
83-
84- // Then
91+ vi . mocked ( useLayout ) . mockReturnValue ( { twoThirds : 20 , oneThird : 10 , fullWidth : 30 } )
92+
93+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Building app" /> )
94+
8595 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
8696 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
8797 Building app ..."
8898 ` )
8999 } )
90100
91101 test ( 'handles narrow terminal width correctly in no-color mode' , async ( ) => {
92- // Given
93- vi . mocked ( useLayout ) . mockReturnValue ( {
94- twoThirds : 15 ,
95- oneThird : 8 ,
96- fullWidth : 23 ,
97- } )
98- const title = 'Installing'
99-
100- // When
101- const { lastFrame} = render ( < LoadingBar title = { title } noColor /> )
102-
103- // Then
102+ vi . mocked ( useLayout ) . mockReturnValue ( { twoThirds : 15 , oneThird : 8 , fullWidth : 23 } )
103+
104+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Installing" noColor /> )
105+
104106 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
105107 "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇
106108 Installing ..."
107109 ` )
108110 } )
109111
110112 test ( 'handles very narrow terminal width in no-color mode' , async ( ) => {
111- // Given
112- vi . mocked ( useLayout ) . mockReturnValue ( {
113- twoThirds : 5 ,
114- oneThird : 3 ,
115- fullWidth : 8 ,
116- } )
117- const title = 'Wait'
118-
119- // When
120- const { lastFrame} = render ( < LoadingBar title = { title } noColor /> )
121-
122- // Then
113+ vi . mocked ( useLayout ) . mockReturnValue ( { twoThirds : 5 , oneThird : 3 , fullWidth : 8 } )
114+
115+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Wait" noColor /> )
116+
123117 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
124118 "▁▁▁▂▂
125119 Wait ..."
126120 ` )
127121 } )
128122
129123 test ( 'handles wide terminal width correctly' , async ( ) => {
130- // Given
131- vi . mocked ( useLayout ) . mockReturnValue ( {
132- twoThirds : 100 ,
133- oneThird : 50 ,
134- fullWidth : 150 ,
135- } )
136- const title = 'Synchronizing data'
137-
138- // When
139- const { lastFrame} = render ( < LoadingBar title = { title } /> )
140-
141- // Then
124+ vi . mocked ( useLayout ) . mockReturnValue ( { twoThirds : 100 , oneThird : 50 , fullWidth : 150 } )
125+
126+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Synchronizing data" /> )
127+
142128 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
143129 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
144130 Synchronizing data ..."
145131 ` )
146132 } )
147133
148134 test ( 'handles wide terminal width correctly in no-color mode with pattern repetition' , async ( ) => {
149- // Given
150- vi . mocked ( useLayout ) . mockReturnValue ( {
151- twoThirds : 90 ,
152- oneThird : 45 ,
153- fullWidth : 135 ,
154- } )
155- const title = 'Analyzing dependencies'
156-
157- // When
158- const { lastFrame} = render ( < LoadingBar title = { title } noColor /> )
159-
160- // Then
135+ vi . mocked ( useLayout ) . mockReturnValue ( { twoThirds : 90 , oneThird : 45 , fullWidth : 135 } )
136+
137+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Analyzing dependencies" noColor /> )
138+
161139 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
162140 "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁
163141 Analyzing dependencies ..."
164142 ` )
165143 } )
166144
167145 test ( 'renders correctly with empty title' , async ( ) => {
168- // Given
169- const title = ''
170-
171- // When
172- const { lastFrame} = render ( < LoadingBar title = { title } /> )
146+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "" /> )
173147
174- // Then
175148 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
176149 "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
177150 ..."
178151 ` )
179152 } )
180153
181154 test ( 'noColor prop overrides shouldDisplayColors when both would show colors' , async ( ) => {
182- // Given
183155 vi . mocked ( shouldDisplayColors ) . mockReturnValue ( true )
184- const title = 'Testing override'
185156
186- // When
187- const { lastFrame} = render ( < LoadingBar title = { title } noColor /> )
157+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "Testing override" noColor /> )
188158
189- // Then
190159 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `
191160 "▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅
192161 Testing override ..."
193162 ` )
194163 } )
195164
196165 test ( 'renders consistently with same props' , async ( ) => {
197- // Given
198- const title = 'Consistent test'
199- const props = { title, noColor : false }
166+ const props = { title : 'Consistent test' , noColor : false }
200167
201- // When
202- const { lastFrame : frame1 } = render ( < LoadingBar { ...props } /> )
203- const { lastFrame : frame2 } = render ( < LoadingBar { ...props } /> )
168+ const { lastFrame : frame1 } = renderWithTTY ( < LoadingBar { ...props } /> )
169+ const { lastFrame : frame2 } = renderWithTTY ( < LoadingBar { ...props } /> )
204170
205- // Then
206171 expect ( frame1 ( ) ) . toBe ( frame2 ( ) )
207172 } )
208173
209174 test ( 'hides progress bar when noProgressBar is true' , async ( ) => {
210- // Given
211175 vi . mocked ( shouldDisplayColors ) . mockReturnValue ( true )
212- const title = 'task 1'
213176
214- // When
215- const { lastFrame} = render ( < LoadingBar title = { title } noProgressBar /> )
177+ const { lastFrame} = renderWithTTY ( < LoadingBar title = "task 1" noProgressBar /> )
216178
217- // Then
218179 expect ( unstyled ( lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `"task 1 ..."` )
219180 } )
181+
182+ test ( 'renders only title text without animated progress bar in non-TTY environments' , async ( ) => {
183+ const stdout = createNonTTYStdout ( )
184+
185+ const renderInstance = render ( < LoadingBar title = "Installing dependencies" /> , { stdout} )
186+
187+ expect ( unstyled ( stdout . lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `"Installing dependencies ..."` )
188+ renderInstance . unmount ( )
189+ } )
190+
191+ test ( 'renders only title text in non-TTY even when noColor and noProgressBar are not set' , async ( ) => {
192+ const stdout = createNonTTYStdout ( )
193+ vi . mocked ( shouldDisplayColors ) . mockReturnValue ( true )
194+
195+ const renderInstance = render ( < LoadingBar title = "Generating extension" /> , { stdout} )
196+
197+ expect ( unstyled ( stdout . lastFrame ( ) ! ) ) . toMatchInlineSnapshot ( `"Generating extension ..."` )
198+ renderInstance . unmount ( )
199+ } )
200+
201+ test ( 'keeps animated progress bar when Ink renders to a TTY stream (e.g. renderTasksToStdErr)' , async ( ) => {
202+ // renderTasksToStdErr passes process.stderr as Ink's stdout option.
203+ // useStdout() returns that stream, so the TTY check uses the correct stream.
204+ const ttyStream = createTTYStdout ( )
205+
206+ const renderInstance = render ( < LoadingBar title = "Uploading theme" /> , { stdout : ttyStream } )
207+
208+ const frame = unstyled ( ttyStream . lastFrame ( ) ! )
209+ expect ( frame ) . toContain ( '▀' )
210+ expect ( frame ) . toContain ( 'Uploading theme ...' )
211+ renderInstance . unmount ( )
212+ } )
220213} )
0 commit comments