Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/app/src/cli/prompts/deploy-release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,75 @@ describe('deployOrReleaseConfirmationPrompt', () => {
)
expect(result).toBe(true)
})

test('and no force with removed targets should display the dangerous confirmation prompt with target change details', async () => {
// Given
const breakdownInfo = buildEmptyBreakdownInfo()
breakdownInfo.extensionIdentifiersBreakdown.toUpdate.push(
buildExtensionBreakdownInfo('checkout-ui', undefined, {
removedTargets: ['purchase.checkout.block.render'],
}),
)
breakdownInfo.extensionIdentifiersBreakdown.unchanged.push(
buildExtensionBreakdownInfo('unchanged extension', undefined),
)

const renderDangerousConfirmationPromptSpyOn = vi
.spyOn(ui, 'renderDangerousConfirmationPrompt')
.mockResolvedValue(true)
vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {})
const appTitle = 'app title'

// When
const result = await deployOrReleaseConfirmationPrompt({
...breakdownInfo,
appTitle,
release: true,
force: false,
})

// Then
expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Release a new version of app title?',
confirmation: appTitle,
infoTable: expect.arrayContaining([
expect.objectContaining({
header: 'Extensions:',
helperText:
'Removing extension targets can break the experience for merchants who have this extension activated at those targets. Consider removing the extension and creating a new one instead.',
}),
Comment on lines +381 to +390
]),
}),
)
expect(result).toBe(true)
})

test('and no force with added targets should show updated extensions without dangerous prompt', async () => {
// Given
const breakdownInfo = buildEmptyBreakdownInfo()
breakdownInfo.extensionIdentifiersBreakdown.toUpdate.push(
buildExtensionBreakdownInfo('checkout-ui', undefined, {
addedTargets: ['purchase.checkout.footer.render'],
}),
)

const renderConfirmationPromptSpyOn = vi.spyOn(ui, 'renderConfirmationPrompt').mockResolvedValue(true)
vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {})
const appTitle = 'app title'

// When
const result = await deployOrReleaseConfirmationPrompt({
...breakdownInfo,
appTitle,
release: true,
force: false,
})

// Then
expect(renderConfirmationPromptSpyOn).toHaveBeenCalled()
expect(result).toBe(true)
})
})

describe('when no release', () => {
Expand Down
50 changes: 37 additions & 13 deletions packages/app/src/cli/prompts/deploy-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface DeployConfirmationPromptOptions {
extensionsContentPrompt: {
extensionsInfoTable?: InfoTableSection
hasDeletedExtensions: boolean
hasRemovedTargets: boolean
}
configContentPrompt?: {
configInfoTable: InfoTableSection
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function deployOrReleaseConfirmationPrompt({

async function deployConfirmationPrompt({
appTitle,
extensionsContentPrompt: {extensionsInfoTable, hasDeletedExtensions},
extensionsContentPrompt: {extensionsInfoTable, hasDeletedExtensions, hasRemovedTargets},
configContentPrompt,
release,
}: DeployConfirmationPromptOptions): Promise<boolean> {
Expand All @@ -65,13 +66,13 @@ async function deployConfirmationPrompt({
: configContentPrompt.configInfoTable,
)
}
const isDangerous = appTitle !== undefined && hasDeletedExtensions
const isDangerous = appTitle !== undefined && (hasDeletedExtensions || hasRemovedTargets)
if (extensionsInfoTable) {
infoTable.push(
isDangerous
? {...extensionsInfoTable, helperText: 'Removing extensions can permanently delete app user data'}
: extensionsInfoTable,
)
if (isDangerous && hasDeletedExtensions) {
infoTable.push({...extensionsInfoTable, helperText: 'Removing extensions can permanently delete app user data'})
} else {
infoTable.push(extensionsInfoTable)
}
} else {
infoTable.push({header: 'Extensions:', emptyItemsText: 'None', items: []})
}
Expand Down Expand Up @@ -109,12 +110,17 @@ async function buildExtensionsContentPrompt(extensionsContentBreakdown: Extensio
switch (extension.experience) {
case 'dashboard':
return [extension.title, {subdued: `(${preffix}from Partner Dashboard)`}]
case 'extension':
if (extension.uid && extension.uid.length > 0) {
return `${extension.title} (uid: ${extension.uid})`
} else {
return extension.title
case 'extension': {
const label =
extension.uid && extension.uid.length > 0 ? `${extension.title} (uid: ${extension.uid})` : extension.title

// Append target change details if present
const targetDetails = buildTargetChangeDetails(extension)
if (targetDetails) {
return [label, {subdued: targetDetails}]
}
return label
}
}
}
let extensionsInfoTable
Expand All @@ -127,10 +133,17 @@ async function buildExtensionsContentPrompt(extensionsContentBreakdown: Extensio
const extensionsInfo = buildDeployReleaseInfoTableSection(section)

const hasDeletedExtensions = onlyRemote.length > 0
const hasRemovedTargets = toUpdate.some((ext) => ext.removedTargets && ext.removedTargets.length > 0)
if (extensionsInfo.length > 0) {
extensionsInfoTable = {
header: 'Extensions:',
items: extensionsInfo,
...(hasRemovedTargets
? {
helperText:
'Removing extension targets can break the experience for merchants who have this extension activated at those targets. Consider removing the extension and creating a new one instead.',
}
: {}),
}
}

Expand All @@ -140,7 +153,18 @@ async function buildExtensionsContentPrompt(extensionsContentBreakdown: Extensio
cmd_deploy_confirm_removed_registrations: onlyRemote.length,
}))

return {extensionsInfoTable, hasDeletedExtensions}
return {extensionsInfoTable, hasDeletedExtensions, hasRemovedTargets}
}

function buildTargetChangeDetails(extension: ExtensionIdentifierBreakdownInfo): string | undefined {
const parts: string[] = []
if (extension.removedTargets && extension.removedTargets.length > 0) {
parts.push(`removing: ${extension.removedTargets.join(', ')}`)
}
if (extension.addedTargets && extension.addedTargets.length > 0) {
parts.push(`adding: ${extension.addedTargets.join(', ')}`)
}
return parts.length > 0 ? `(${parts.join('; ')})` : undefined
}

async function buildConfigContentPrompt(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,77 @@ describe('extensionsIdentifiersDeployBreakdown', () => {
remoteExtensionsRegistrations: remoteExtensionRegistrations.app,
})
})

test('and there is an active version with target changes, extension should be returned as toUpdate with target change details', async () => {
// Given
const MODULE_CLI_A_WITH_TARGETS: AppModuleVersion = {
registrationId: 'A',
registrationUuid: 'UUID_A',
registrationTitle: 'Checkout post purchase',
type: 'checkout_post_purchase',
config: {
extension_points: ['purchase.checkout.block.render', 'purchase.checkout.header.render'],
},
specification: {
identifier: 'checkout_post_purchase',
name: 'Post purchase UI extension',
experience: 'extension',
options: {
managementExperience: 'cli',
},
},
}

// Create a local extension with only one target (removing purchase.checkout.header.render)
const localExtWithTargetChange = await testUIExtension({
directory: '/EXTENSION_A_TARGETS',
configuration: {
name: 'EXTENSION A',
type: 'checkout_ui_extension',
extension_points: ['purchase.checkout.block.render'],
metafields: [],
},
entrySourceFilePath: '',
devUUID: 'devUUID',
})
const localExtensions = [localExtWithTargetChange, EXTENSION_A_2]

const extensionsToConfirm = {
validMatches: {[localExtWithTargetChange.localIdentifier]: 'UUID_A'},
dashboardOnlyExtensions: [] as RemoteSource[],
extensionsToCreate: [EXTENSION_A_2],
didMigrateDashboardExtensions: false,
}
vi.mocked(ensureExtensionsIds).mockResolvedValue(extensionsToConfirm)
const remoteExtensionRegistrations = {
app: {
extensionRegistrations: [REGISTRATION_A],
configurationRegistrations: [],
dashboardManagedExtensionRegistrations: [],
},
}
const activeAppVersion = {
appModuleVersions: [MODULE_CONFIG_A, MODULE_CLI_A_WITH_TARGETS],
}
const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({
appExtensionRegistrations: (_app: MinimalAppIdentifiers) => Promise.resolve(remoteExtensionRegistrations),
activeAppVersion: (_app: MinimalAppIdentifiers) => Promise.resolve(activeAppVersion),
})

// When
const result = await extensionsIdentifiersDeployBreakdown(
await options({uiExtensions: localExtensions, developerPlatformClient, activeAppVersion}),
)

// Then
const updatedExtensions = result.extensionIdentifiersBreakdown.toUpdate
expect(updatedExtensions.length).toBeGreaterThanOrEqual(1)
const extensionWithTargetChange = updatedExtensions.find(
(ext) => ext.removedTargets && ext.removedTargets.length > 0,
)
expect(extensionWithTargetChange).toBeDefined()
expect(extensionWithTargetChange!.removedTargets).toEqual(['purchase.checkout.header.render'])
})
})
})

Expand Down
Loading
Loading