11import ThemeCommand from './theme-command.js'
2+ import { ensureThemeStore } from './theme-store.js'
23import { describe , vi , expect , test , beforeEach } from 'vitest'
34import { Config , Flags } from '@oclif/core'
45import { AdminSession , ensureAuthenticatedThemes } from '@shopify/cli-kit/node/session'
56import { loadEnvironment } from '@shopify/cli-kit/node/environments'
6- import { renderConcurrent } from '@shopify/cli-kit/node/ui'
7+ import { renderConcurrent , renderConfirmationPrompt , renderError } from '@shopify/cli-kit/node/ui'
78import type { Writable } from 'stream'
89
910vi . mock ( '@shopify/cli-kit/node/session' )
1011vi . mock ( '@shopify/cli-kit/node/environments' )
1112vi . mock ( '@shopify/cli-kit/node/ui' )
13+ vi . mock ( './theme-store.js' )
1214
1315const CommandConfig = new Config ( { root : __dirname } )
1416
@@ -29,10 +31,30 @@ class TestThemeCommand extends ThemeCommand {
2931
3032 static multiEnvironmentsFlags = [ 'store' ]
3133
32- commandCalls : { flags : any ; session : AdminSession ; context ?: any } [ ] = [ ]
34+ commandCalls : { flags : any ; session : AdminSession ; multiEnvironment ?: boolean ; context ?: any } [ ] = [ ]
3335
34- async command ( flags : any , session : AdminSession , context ?: { stdout ?: Writable ; stderr ?: Writable } ) : Promise < void > {
35- this . commandCalls . push ( { flags, session, context} )
36+ async command (
37+ flags : any ,
38+ session : AdminSession ,
39+ multiEnvironment ?: boolean ,
40+ context ?: { stdout ?: Writable ; stderr ?: Writable } ,
41+ ) : Promise < void > {
42+ this . commandCalls . push ( { flags, session, multiEnvironment, context} )
43+
44+ if ( flags . environment && flags . environment [ 0 ] === 'command-error' ) {
45+ throw new Error ( 'Mocking a command error' )
46+ }
47+ }
48+ }
49+
50+ class TestThemeCommandWithForce extends TestThemeCommand {
51+ static flags = {
52+ ...TestThemeCommand . flags ,
53+ force : Flags . boolean ( {
54+ char : 'f' ,
55+ description : 'Skip confirmation' ,
56+ env : 'SHOPIFY_FLAG_FORCE' ,
57+ } ) ,
3658 }
3759}
3860
@@ -44,6 +66,7 @@ describe('ThemeCommand', () => {
4466 token : 'test-token' ,
4567 storeFqdn : 'test-store.myshopify.com' ,
4668 }
69+ vi . mocked ( ensureThemeStore ) . mockReturnValue ( 'test-store.myshopify.com' )
4770 vi . mocked ( ensureAuthenticatedThemes ) . mockResolvedValue ( mockSession )
4871 } )
4972
@@ -52,7 +75,6 @@ describe('ThemeCommand', () => {
5275 // Given
5376 await CommandConfig . load ( )
5477 const command = new TestThemeCommand ( [ ] , CommandConfig )
55-
5678 // When
5779 await command . run ( )
5880
@@ -125,4 +147,189 @@ describe('ThemeCommand', () => {
125147 )
126148 } )
127149 } )
150+
151+ describe ( 'multi environment' , ( ) => {
152+ test ( 'commands with --force flag should not prompt for confirmation' , async ( ) => {
153+ // Given
154+ const environmentConfig = { store : 'store.myshopify.com' }
155+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( environmentConfig )
156+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
157+ vi . mocked ( renderConcurrent ) . mockResolvedValue ( undefined )
158+
159+ await CommandConfig . load ( )
160+ const command = new TestThemeCommandWithForce (
161+ [ '--environment' , 'development' , '--environment' , 'staging' , '--force' ] ,
162+ CommandConfig ,
163+ )
164+
165+ // When
166+ await command . run ( )
167+
168+ // Then
169+ expect ( renderConfirmationPrompt ) . not . toHaveBeenCalled ( )
170+ expect ( renderConcurrent ) . toHaveBeenCalledOnce ( )
171+ expect ( renderConcurrent ) . toHaveBeenCalledWith (
172+ expect . objectContaining ( {
173+ processes : expect . arrayContaining ( [
174+ expect . objectContaining ( { prefix : 'development' } ) ,
175+ expect . objectContaining ( { prefix : 'staging' } ) ,
176+ ] ) ,
177+ showTimestamps : true ,
178+ } ) ,
179+ )
180+ } )
181+
182+ test ( 'commands that do not allow --force flag should not prompt for confirmation' , async ( ) => {
183+ // Given
184+ const environmentConfig = { store : 'store.myshopify.com' }
185+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( environmentConfig )
186+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
187+ vi . mocked ( renderConcurrent ) . mockResolvedValue ( undefined )
188+
189+ await CommandConfig . load ( )
190+ const command = new TestThemeCommand ( [ '--environment' , 'development' , '--environment' , 'staging' ] , CommandConfig )
191+
192+ // When
193+ await command . run ( )
194+
195+ // Then
196+ expect ( renderConfirmationPrompt ) . not . toHaveBeenCalled ( )
197+ expect ( renderConcurrent ) . toHaveBeenCalledOnce ( )
198+ } )
199+
200+ test ( 'commands without --force flag that allow it should prompt for confirmation' , async ( ) => {
201+ // Given
202+ const environmentConfig = { store : 'store.myshopify.com' }
203+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( environmentConfig )
204+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
205+ vi . mocked ( renderConcurrent ) . mockResolvedValue ( undefined )
206+
207+ await CommandConfig . load ( )
208+ const command = new TestThemeCommandWithForce (
209+ [ '--environment' , 'development' , '--environment' , 'staging' ] ,
210+ CommandConfig ,
211+ )
212+
213+ // When
214+ await command . run ( )
215+
216+ // Then
217+ expect ( renderConfirmationPrompt ) . toHaveBeenCalledOnce ( )
218+ expect ( renderConfirmationPrompt ) . toHaveBeenCalledWith (
219+ expect . objectContaining ( {
220+ message : expect . any ( Array ) ,
221+ confirmationMessage : 'Yes, proceed' ,
222+ cancellationMessage : 'Cancel' ,
223+ } ) ,
224+ )
225+ expect ( renderConcurrent ) . toHaveBeenCalledOnce ( )
226+ } )
227+
228+ test ( 'should not execute command if confirmation is cancelled' , async ( ) => {
229+ // Given
230+ const environmentConfig = { store : 'store.myshopify.com' }
231+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( environmentConfig )
232+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( false )
233+ vi . mocked ( renderConcurrent ) . mockResolvedValue ( undefined )
234+
235+ await CommandConfig . load ( )
236+ const command = new TestThemeCommandWithForce (
237+ [ '--environment' , 'development' , '--environment' , 'staging' ] ,
238+ CommandConfig ,
239+ )
240+
241+ // When
242+ await command . run ( )
243+
244+ // Then
245+ expect ( renderConfirmationPrompt ) . toHaveBeenCalledOnce ( )
246+ expect ( renderConcurrent ) . not . toHaveBeenCalled ( )
247+ } )
248+
249+ test ( 'should not execute commands in environments that are missing required flags' , async ( ) => {
250+ // Given
251+ vi . mocked ( loadEnvironment )
252+ . mockResolvedValueOnce ( { store : 'store1.myshopify.com' } )
253+ . mockResolvedValueOnce ( { } )
254+ . mockResolvedValueOnce ( { store : 'store3.myshopify.com' } )
255+
256+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
257+ vi . mocked ( renderConcurrent ) . mockResolvedValue ( undefined )
258+
259+ await CommandConfig . load ( )
260+ const command = new TestThemeCommand (
261+ [ '--environment' , 'development' , '--environment' , 'env-missing-store' , '--environment' , 'production' ] ,
262+ CommandConfig ,
263+ )
264+
265+ // When
266+ await command . run ( )
267+
268+ // Then
269+ const renderConcurrentProcesses = vi . mocked ( renderConcurrent ) . mock . calls [ 0 ] ?. [ 0 ] ?. processes
270+ expect ( renderConcurrentProcesses ) . toHaveLength ( 2 )
271+ expect ( renderConcurrentProcesses ?. map ( ( process ) => process . prefix ) ) . toEqual ( [ 'development' , 'production' ] )
272+ } )
273+
274+ test ( 'commands error gracefully and continue with other environments' , async ( ) => {
275+ // Given
276+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( { store : 'store.myshopify.com' } )
277+
278+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
279+ vi . mocked ( renderConcurrent ) . mockImplementation ( async ( { processes} ) => {
280+ for ( const process of processes ) {
281+ // eslint-disable-next-line no-await-in-loop
282+ await process . action ( { } as Writable , { } as Writable , { } as any )
283+ }
284+ } )
285+
286+ await CommandConfig . load ( )
287+ const command = new TestThemeCommand (
288+ [ '--environment' , 'command-error' , '--environment' , 'development' , '--environment' , 'production' ] ,
289+ CommandConfig ,
290+ )
291+
292+ // When
293+ await command . run ( )
294+
295+ // Then
296+ const renderConcurrentProcesses = vi . mocked ( renderConcurrent ) . mock . calls [ 0 ] ?. [ 0 ] ?. processes
297+ expect ( renderConcurrentProcesses ) . toHaveLength ( 3 )
298+ expect ( renderConcurrentProcesses ?. map ( ( process ) => process . prefix ) ) . toEqual ( [
299+ 'command-error' ,
300+ 'development' ,
301+ 'production' ,
302+ ] )
303+ } )
304+
305+ test ( 'error messages contain the environment name' , async ( ) => {
306+ // Given
307+ const environmentConfig = { store : 'store.myshopify.com' }
308+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( environmentConfig )
309+ vi . mocked ( renderConfirmationPrompt ) . mockResolvedValue ( true )
310+
311+ vi . mocked ( renderConcurrent ) . mockImplementation ( async ( { processes} ) => {
312+ for ( const process of processes ) {
313+ // eslint-disable-next-line no-await-in-loop
314+ await process . action ( { } as Writable , { } as Writable , { } as any )
315+ }
316+ } )
317+
318+ await CommandConfig . load ( )
319+ const command = new TestThemeCommand (
320+ [ '--environment' , 'command-error' , '--environment' , 'development' ] ,
321+ CommandConfig ,
322+ )
323+
324+ // When
325+ await command . run ( )
326+
327+ // Then
328+ expect ( renderError ) . toHaveBeenCalledWith (
329+ expect . objectContaining ( {
330+ body : [ 'Environment command-error failed: \n\nMocking a command error' ] ,
331+ } ) ,
332+ )
333+ } )
334+ } )
128335} )
0 commit comments