@@ -5,15 +5,20 @@ import {findOrSelectTheme} from '../../utilities/theme-selector.js'
55import { ensureThemeStore } from '../../utilities/theme-store.js'
66import { openURL } from '@shopify/cli-kit/node/system'
77import { runWithCommandEventsForCommand } from '@shopify/cli-kit/node/command-events'
8- import { renderSuccess } from '@shopify/cli-kit/node/ui'
8+ import { renderSuccess , renderWarning } from '@shopify/cli-kit/node/ui'
99import { buildTheme } from '@shopify/cli-kit/node/themes/factories'
1010import { recordEvent } from '@shopify/cli-kit/node/analytics'
1111import { ensureAuthenticatedThemes } from '@shopify/cli-kit/node/session'
1212import { withCapturedStandardStreams } from '@shopify/cli-kit/node/testing/output'
1313import { Config } from '@oclif/core'
14+ import { inTemporaryDirectory , writeFile } from '@shopify/cli-kit/node/fs'
15+ import { joinPath } from '@shopify/cli-kit/node/path'
1416import { describe , vi , expect , test , beforeEach } from 'vitest'
1517
16- vi . mock ( '@shopify/cli-kit/node/ui' )
18+ vi . mock ( '@shopify/cli-kit/node/ui' , async ( importOriginal ) => {
19+ const actual = await importOriginal < typeof import ( '@shopify/cli-kit/node/ui' ) > ( )
20+ return { ...actual , renderSuccess : vi . fn ( ) , renderWarning : vi . fn ( actual . renderWarning ) }
21+ } )
1722vi . mock ( '@shopify/cli-kit/node/system' , async ( importOriginal ) => ( {
1823 ...( await importOriginal < typeof import ( '@shopify/cli-kit/node/system' ) > ( ) ) ,
1924 openURL : vi . fn ( ) ,
@@ -53,6 +58,36 @@ describe('Preview', () => {
5358 vi . mocked ( devWithOverrideFile ) . mockResolvedValue ( result )
5459 } )
5560
61+ test . each ( [ 'default' , 'staging' ] . flatMap ( ( environment ) => [ false , true ] . map ( ( json ) => ( { environment, json} ) ) ) ) (
62+ 'uses the remembered store for the $environment environment with json=$json' ,
63+ async ( { environment, json} ) => {
64+ const environments = await import ( '@shopify/cli-kit/node/environments' )
65+ const actual = await vi . importActual < typeof environments > ( '@shopify/cli-kit/node/environments' )
66+ vi . mocked ( environments . loadEnvironment ) . mockImplementation ( actual . loadEnvironment )
67+ vi . mocked ( environments . environmentFilePath ) . mockImplementation ( actual . environmentFilePath )
68+ await inTemporaryDirectory ( async ( directory ) => {
69+ await writeFile (
70+ joinPath ( directory , 'shopify.theme.toml' ) ,
71+ `[environments.${ environment } ]\ntheme = "2"\noverrides = "/path/to/overrides.json"\n` ,
72+ )
73+ const config = new Config ( { root : __dirname } )
74+ await config . load ( )
75+ const argv = [
76+ '--path' ,
77+ directory ,
78+ ...( environment === 'default' ? [ ] : [ '--environment' , environment ] ) ,
79+ ...( json ? [ '--json' ] : [ ] ) ,
80+ ]
81+ await withCapturedStandardStreams ( async ( ) => {
82+ await runWithCommandEventsForCommand ( argv , ( ) => new Preview ( argv , config ) . run ( ) )
83+ } )
84+ expect ( ensureThemeStore ) . toHaveBeenCalledWith ( expect . objectContaining ( { store : undefined } ) )
85+ expect ( ensureAuthenticatedThemes ) . toHaveBeenCalledWith ( adminSession . storeFqdn , undefined )
86+ expect ( devWithOverrideFile ) . toHaveBeenCalledWith ( expect . objectContaining ( { adminSession, themeId : '2' } ) )
87+ } )
88+ } ,
89+ )
90+
5691 test ( 'calls devWithOverrideFile with minimum options passed into the command' , async ( ) => {
5792 const expectedTheme = buildTheme ( { id : 5 , name : 'Expected Theme' , role : 'unpublished' } ) !
5893 vi . mocked ( findOrSelectTheme ) . mockResolvedValue ( expectedTheme )
@@ -204,14 +239,51 @@ describe('Preview', () => {
204239 . split ( '\n' )
205240 . map ( ( line ) => JSON . parse ( line ) )
206241 expect ( events ) . toMatchObject ( [
207- { type : 'diagnostic' , level : 'warning' , message : `Failed to open theme preview.\n${ error . stack } ` } ,
242+ { type : 'diagnostic' , level : 'warning' , message : `Failed to open theme preview.\n\n ${ error . stack } ` } ,
208243 ] )
209244 expect ( JSON . parse ( stdout ( ) ) ) . toEqual ( {
210245 status : 'success' ,
211246 preview : { id : result . preview_identifier , url : result . url } ,
212247 } )
213248 } )
214249 } )
250+
251+ test . each ( [ false , true ] ) ( 'reports the browser open result in JSON diagnostics (opened: %s)' , async ( opened ) => {
252+ vi . mocked ( openURL ) . mockResolvedValue ( opened )
253+
254+ await withCapturedStandardStreams ( async ( { stdout, stderr} ) => {
255+ await runWithCommandEventsForCommand ( [ '--json' ] , ( ) =>
256+ run ( [ '--overrides=/path/to/overrides.json' , '--theme=2' , '--json' , '--open' ] ) ,
257+ )
258+
259+ expect ( JSON . parse ( stdout ( ) ) ) . toEqual ( {
260+ status : 'success' ,
261+ preview : { id : result . preview_identifier , url : result . url } ,
262+ } )
263+ if ( opened ) {
264+ expect ( stderr ( ) ) . toBe ( '' )
265+ } else {
266+ expect ( JSON . parse ( stderr ( ) ) ) . toMatchObject ( {
267+ type : 'diagnostic' ,
268+ level : 'warning' ,
269+ message : expect . stringContaining ( `Open this URL manually: ${ result . url } ` ) ,
270+ } )
271+ }
272+ } )
273+ expect ( openURL ) . toHaveBeenCalledWith ( result . url )
274+ } )
275+
276+ test ( 'warns in text mode when the browser could not be opened' , async ( ) => {
277+ vi . mocked ( openURL ) . mockResolvedValue ( false )
278+
279+ await run ( [ '--overrides=/path/to/overrides.json' , '--theme=2' , '--open' ] )
280+
281+ expect ( renderWarning ) . toHaveBeenCalledWith ( {
282+ headline : 'Failed to open theme preview.' ,
283+ body : expect . stringContaining ( `Open this URL manually: ${ result . url } ` ) ,
284+ } )
285+ expect ( renderSuccess ) . toHaveBeenCalledOnce ( )
286+ } )
215287} )
216288
217289test ( 'returns one explicit environment result' , async ( ) => {
@@ -249,5 +321,81 @@ test.each([['--theme=2'], ['--overrides=/path/to/overrides.json']])(
249321 'Specify both --theme and --overrides, either as flags or in an environment.' ,
250322 )
251323 expect ( devWithOverrideFile ) . not . toHaveBeenCalled ( )
324+ expect ( ensureAuthenticatedThemes ) . not . toHaveBeenCalled ( )
325+ } ,
326+ )
327+
328+ test . each ( [ 'none' , 'validation' , 'store' , 'execution' ] ) (
329+ 'retains every preview environment with %s failures' ,
330+ async ( failure ) => {
331+ const { loadEnvironment} = await import ( '@shopify/cli-kit/node/environments' )
332+ vi . mocked ( loadEnvironment ) . mockImplementation ( async ( environment ) => ( {
333+ ...( failure === 'store' && environment === 'staging' ? { } : { store : `${ environment } .myshopify.com` } ) ,
334+ password : 'token' ,
335+ theme : '2' ,
336+ ...( failure === 'validation' && environment === 'staging' ? { } : { overrides : '/path/to/overrides.json' } ) ,
337+ } ) )
338+ vi . mocked ( ensureThemeStore ) . mockImplementation ( ( { store} ) => store ! )
339+ vi . mocked ( ensureAuthenticatedThemes ) . mockImplementation ( async ( storeFqdn ) => ( { storeFqdn, token : 'token' } ) )
340+ vi . mocked ( findOrSelectTheme ) . mockResolvedValue ( namedTheme )
341+ vi . mocked ( devWithOverrideFile ) . mockImplementation ( async ( { adminSession} ) => {
342+ if ( failure === 'execution' && adminSession . storeFqdn === 'staging.myshopify.com' ) {
343+ throw new Error ( 'Preview failed' )
344+ }
345+ return result
346+ } )
347+ const config = new Config ( { root : __dirname } )
348+ await config . load ( )
349+ const argv = [ '--json' , '-e' , 'staging' , '-e' , 'production' ]
350+ const missingFlag = failure === 'store' ? 'store' : 'overrides'
351+ const previousExitCode = process . exitCode
352+ process . exitCode = 0
353+ try {
354+ await withCapturedStandardStreams ( async ( { stdout} ) => {
355+ await runWithCommandEventsForCommand ( argv , ( ) => new Preview ( argv , config ) . run ( ) )
356+ expect ( JSON . parse ( stdout ( ) ) ) . toEqual ( {
357+ environments : [ 'staging' , 'production' ] . map ( ( environment ) => ( {
358+ environment,
359+ ...( environment === 'staging' && failure !== 'none'
360+ ? {
361+ error : expect . objectContaining ( {
362+ message : expect . stringContaining ( failure === 'execution' ? 'Preview failed' : missingFlag ) ,
363+ } ) ,
364+ }
365+ : { result : { status : 'success' , preview : { id : result . preview_identifier , url : result . url } } } ) ,
366+ } ) ) ,
367+ } )
368+ } )
369+ expect ( ensureAuthenticatedThemes ) . toHaveBeenCalledTimes ( [ 'validation' , 'store' ] . includes ( failure ) ? 1 : 2 )
370+ expect ( devWithOverrideFile ) . toHaveBeenCalledTimes ( [ 'validation' , 'store' ] . includes ( failure ) ? 1 : 2 )
371+ expect ( process . exitCode ) . toBe ( failure === 'none' ? 0 : 1 )
372+ } finally {
373+ process . exitCode = previousExitCode
374+ }
375+ } ,
376+ )
377+
378+ test . each ( [ 'theme' , 'overrides' ] ) (
379+ 'validates missing %s in a JSON environment before authentication' ,
380+ async ( missing ) => {
381+ const { loadEnvironment} = await import ( '@shopify/cli-kit/node/environments' )
382+ vi . mocked ( loadEnvironment ) . mockResolvedValue ( {
383+ store : adminSession . storeFqdn ,
384+ ...( missing === 'theme' ? { overrides : '/path/to/overrides.json' } : { theme : '2' } ) ,
385+ } )
386+ const previousExitCode = process . exitCode
387+ try {
388+ await withCapturedStandardStreams ( async ( { stdout} ) => {
389+ await runWithCommandEventsForCommand ( [ '--json' ] , ( ) => run ( [ '--environment=staging' , '--json' ] ) )
390+ expect ( JSON . parse ( stdout ( ) ) ) . toMatchObject ( {
391+ environments : [ { environment : 'staging' , error : { message : expect . stringContaining ( missing ) } } ] ,
392+ } )
393+ } )
394+ expect ( process . exitCode ) . toBe ( 1 )
395+ expect ( ensureAuthenticatedThemes ) . not . toHaveBeenCalled ( )
396+ expect ( devWithOverrideFile ) . not . toHaveBeenCalled ( )
397+ } finally {
398+ process . exitCode = previousExitCode
399+ }
252400 } ,
253401)
0 commit comments