Skip to content

Commit 997d4bb

Browse files
authored
Merge pull request #6124 from Shopify/jb-multi-env
Update multi-environment theme command behaviour
2 parents 1d63429 + b382a89 commit 997d4bb

File tree

4 files changed

+374
-43
lines changed

4 files changed

+374
-43
lines changed

.changeset/breezy-papayas-watch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/theme': minor
3+
'@shopify/cli': minor
4+
---
5+
6+
Allow multi-environment theme commands to accept flags from CLI

.changeset/dry-waves-agree.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/theme': minor
3+
'@shopify/cli': minor
4+
---
5+
6+
Prompt for confirmation before running multi-environment theme commands that allow `--force` flag

packages/theme/src/cli/utilities/theme-command.test.ts

Lines changed: 212 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import ThemeCommand from './theme-command.js'
2+
import {ensureThemeStore} from './theme-store.js'
23
import {describe, vi, expect, test, beforeEach} from 'vitest'
34
import {Config, Flags} from '@oclif/core'
45
import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
56
import {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'
78
import type {Writable} from 'stream'
89

910
vi.mock('@shopify/cli-kit/node/session')
1011
vi.mock('@shopify/cli-kit/node/environments')
1112
vi.mock('@shopify/cli-kit/node/ui')
13+
vi.mock('./theme-store.js')
1214

1315
const 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

Comments
 (0)