@@ -4,9 +4,10 @@ import {testAppLinked, testDeveloperPlatformClient, testUIExtension} from '../mo
44import { OrganizationApp } from '../models/organization.js'
55import { ExtensionRegistration } from '../api/graphql/all_app_extension_registrations.js'
66import { describe , expect , test , vi , beforeEach } from 'vitest'
7- import { fileExistsSync , inTemporaryDirectory } from '@shopify/cli-kit/node/fs'
7+ import { fileExistsSync , inTemporaryDirectory , mkdir } from '@shopify/cli-kit/node/fs'
88import { renderSelectPrompt , renderSuccess } from '@shopify/cli-kit/node/ui'
99import { joinPath } from '@shopify/cli-kit/node/path'
10+ import { AbortSilentError } from '@shopify/cli-kit/node/error'
1011
1112vi . mock ( '@shopify/cli-kit/node/ui' )
1213vi . mock ( './context.js' )
@@ -135,6 +136,141 @@ describe('import-extensions', () => {
135136 } )
136137 } )
137138
139+ test ( 'handles existing directory with user prompt - skip' , async ( ) => {
140+ // Given
141+ const extensions = [ flowExtensionA ]
142+
143+ // When
144+ await inTemporaryDirectory ( async ( tmpDir ) => {
145+ const app = testAppLinked ( { directory : tmpDir } )
146+
147+ // Create the extensions directory
148+ const extensionsDir = joinPath ( tmpDir , 'extensions' )
149+ await mkdir ( extensionsDir )
150+
151+ // Create the specific extension directory
152+ const extensionDir = joinPath ( extensionsDir , 'title-a' )
153+ await mkdir ( extensionDir )
154+
155+ // Mock prompts:
156+ // 1. First prompt: select which extension to migrate (select flowExtensionA by its UUID)
157+ // 2. Second prompt: what to do with existing directory (select skip)
158+ vi . mocked ( renderSelectPrompt )
159+ // Select flowExtensionA
160+ . mockResolvedValueOnce ( 'uuidA' )
161+ // Skip existing directory
162+ . mockResolvedValueOnce ( 'skip' )
163+
164+ await importExtensions ( {
165+ app,
166+ remoteApp : organizationApp ,
167+ developerPlatformClient : testDeveloperPlatformClient ( ) ,
168+ extensionTypes : [ 'flow_action_definition' ] ,
169+ extensions,
170+ buildTomlObject,
171+ } )
172+
173+ // Then - expect the success message to be shown (even for skipped extensions)
174+ expect ( renderSuccess ) . toHaveBeenCalledWith ( {
175+ headline : [ 'Imported the following extensions from the dashboard:' ] ,
176+ body : '• "titleA" at: extensions/title-a' ,
177+ } )
178+
179+ // The toml file should not be created since we skipped
180+ const tomlPathA = joinPath ( tmpDir , 'extensions' , 'title-a' , 'shopify.extension.toml' )
181+ expect ( fileExistsSync ( tomlPathA ) ) . toBe ( false )
182+ } )
183+ } )
184+
185+ test ( 'handles existing directory with write option' , async ( ) => {
186+ // Given
187+ const extensions = [ flowExtensionA ]
188+
189+ // When
190+ await inTemporaryDirectory ( async ( tmpDir ) => {
191+ const app = testAppLinked ( { directory : tmpDir } )
192+
193+ // Create the extensions directory
194+ const extensionsDir = joinPath ( tmpDir , 'extensions' )
195+ await mkdir ( extensionsDir )
196+
197+ // Create the specific extension directory
198+ const extensionDir = joinPath ( extensionsDir , 'title-a' )
199+ await mkdir ( extensionDir )
200+
201+ // Mock prompts:
202+ // 1. First prompt: select which extension to migrate (select flowExtensionA by its UUID)
203+ // 2. Second prompt: what to do with existing directory (select write)
204+ vi . mocked ( renderSelectPrompt )
205+ // Select flowExtensionA
206+ . mockResolvedValueOnce ( 'uuidA' )
207+ // Write/overwrite existing directory
208+ . mockResolvedValueOnce ( 'write' )
209+
210+ await importExtensions ( {
211+ app,
212+ remoteApp : organizationApp ,
213+ developerPlatformClient : testDeveloperPlatformClient ( ) ,
214+ extensionTypes : [ 'flow_action_definition' ] ,
215+ extensions,
216+ buildTomlObject,
217+ } )
218+
219+ // Then - expect the success message to be shown
220+ expect ( renderSuccess ) . toHaveBeenCalledWith ( {
221+ headline : [ 'Imported the following extensions from the dashboard:' ] ,
222+ body : '• "titleA" at: extensions/title-a' ,
223+ } )
224+
225+ // The toml file should be created since we wrote/overwrote
226+ const tomlPathA = joinPath ( tmpDir , 'extensions' , 'title-a' , 'shopify.extension.toml' )
227+ expect ( fileExistsSync ( tomlPathA ) ) . toBe ( true )
228+ } )
229+ } )
230+
231+ test ( 'handles existing directory with cancel option' , async ( ) => {
232+ // Given
233+ const extensions = [ flowExtensionA ]
234+
235+ // When
236+ await inTemporaryDirectory ( async ( tmpDir ) => {
237+ const app = testAppLinked ( { directory : tmpDir } )
238+
239+ // Create the extensions directory
240+ const extensionsDir = joinPath ( tmpDir , 'extensions' )
241+ await mkdir ( extensionsDir )
242+
243+ // Create the specific extension directory
244+ const extensionDir = joinPath ( extensionsDir , 'title-a' )
245+ await mkdir ( extensionDir )
246+
247+ // Mock prompts:
248+ // 1. First prompt: select which extension to migrate (select flowExtensionA by its UUID)
249+ // 2. Second prompt: what to do with existing directory (select cancel)
250+ vi . mocked ( renderSelectPrompt )
251+ // Select flowExtensionA
252+ . mockResolvedValueOnce ( 'uuidA' )
253+ // Cancel the operation
254+ . mockResolvedValueOnce ( 'cancel' )
255+
256+ // Then - expect the function to throw an AbortSilentError
257+ await expect (
258+ importExtensions ( {
259+ app,
260+ remoteApp : organizationApp ,
261+ developerPlatformClient : testDeveloperPlatformClient ( ) ,
262+ extensionTypes : [ 'flow_action_definition' ] ,
263+ extensions,
264+ buildTomlObject,
265+ } ) ,
266+ ) . rejects . toThrow ( AbortSilentError )
267+
268+ // The toml file should not be created since we cancelled
269+ const tomlPathA = joinPath ( tmpDir , 'extensions' , 'title-a' , 'shopify.extension.toml' )
270+ expect ( fileExistsSync ( tomlPathA ) ) . toBe ( false )
271+ } )
272+ } )
273+
138274 test ( 'selecting All imports all extensions' , async ( ) => {
139275 // Given
140276 const extensions = [
0 commit comments