@spinnaker/ecs 2026.2.2 → 2026.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/dist/ecs.module.d.ts +3 -1
  2. package/dist/ecsCluster/ecsCluster.read.service.d.ts +2 -3
  3. package/dist/iamRoles/iamRole.read.service.d.ts +1 -2
  4. package/dist/index.d.ts +5 -0
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/instance/details/EcsInstanceDetails.d.ts +50 -0
  8. package/dist/loadBalancer/loadBalancer.transformer.d.ts +1 -5
  9. package/dist/loadBalancer/targetGroup.states.d.ts +2 -1
  10. package/dist/metricAlarm/metricAlarm.read.service.d.ts +1 -2
  11. package/dist/pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage.d.ts +1 -0
  12. package/dist/pipeline/stages/common/EcsServerGroupStepLabel.d.ts +7 -0
  13. package/dist/pipeline/stages/common/EcsStageConfigs.d.ts +11 -0
  14. package/dist/pipeline/stages/destroyAsg/ecsDestroyAsgStage.d.ts +1 -2
  15. package/dist/pipeline/stages/disableAsg/ecsDisableAsgStage.d.ts +1 -2
  16. package/dist/pipeline/stages/disableCluster/ecsDisableClusterStage.d.ts +1 -2
  17. package/dist/pipeline/stages/enableAsg/ecsEnableAsgStage.d.ts +1 -2
  18. package/dist/pipeline/stages/findImageFromTags/EcsFindImageFromTagsExecutionDetails.d.ts +5 -0
  19. package/dist/pipeline/stages/findImageFromTags/ecsFindImageFromTagStage.d.ts +1 -2
  20. package/dist/pipeline/stages/resizeAsg/ecsResizeAsgStage.d.ts +1 -2
  21. package/dist/pipeline/stages/scaleDownCluster/ecsScaleDownClusterStage.d.ts +1 -2
  22. package/dist/pipeline/stages/shrinkCluster/ecsShrinkClusterStage.d.ts +1 -2
  23. package/dist/placementStrategy/placementStrategy.service.d.ts +0 -1
  24. package/dist/secrets/secret.read.service.d.ts +1 -2
  25. package/dist/securityGroup/details/EcsSecurityGroupDetails.d.ts +45 -0
  26. package/dist/securityGroup/securityGroup.reader.d.ts +0 -1
  27. package/dist/securityGroup/securityGroup.transformer.d.ts +3 -2
  28. package/dist/serverGroup/configure/dockerImage.util.d.ts +3 -0
  29. package/dist/serverGroup/configure/serverGroupCommandBuilder.service.d.ts +111 -2
  30. package/dist/serverGroup/configure/serverGroupConfiguration.service.d.ts +13 -63
  31. package/dist/serverGroup/configure/wizard/EcsCloneServerGroupModal.d.ts +64 -0
  32. package/dist/serverGroup/configure/wizard/capacityProvider/CapacityProvider.d.ts +29 -4
  33. package/dist/serverGroup/configure/wizard/container/Container.d.ts +3 -3
  34. package/dist/serverGroup/configure/wizard/networking/Networking.d.ts +3 -3
  35. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  36. package/dist/serverGroup/configure/wizard/pages/BasicSettings.d.ts +3 -0
  37. package/dist/serverGroup/configure/wizard/pages/ContainerSettings.d.ts +3 -0
  38. package/dist/serverGroup/configure/wizard/pages/HorizontalScalingSettings.d.ts +3 -0
  39. package/dist/serverGroup/configure/wizard/pages/LoggingSettings.d.ts +3 -0
  40. package/dist/serverGroup/configure/wizard/pages/NetworkingSettings.d.ts +3 -0
  41. package/dist/serverGroup/configure/wizard/pages/ServiceDiscoverySettings.d.ts +3 -0
  42. package/dist/serverGroup/configure/wizard/pages/TaskDefinitionSettings.d.ts +3 -0
  43. package/dist/serverGroup/configure/wizard/pages/common.d.ts +8 -0
  44. package/dist/serverGroup/configure/wizard/pages/validation.d.ts +22 -0
  45. package/dist/serverGroup/configure/wizard/serviceDiscovery/ServiceDiscovery.d.ts +5 -3
  46. package/dist/serverGroup/configure/wizard/taskDefinition/TaskDefinition.d.ts +3 -3
  47. package/dist/serverGroup/details/EcsServerGroupActions.d.ts +6 -0
  48. package/dist/serverGroup/details/ecsServerGroupDetailsGetter.d.ts +3 -0
  49. package/dist/serverGroup/details/resize/EcsResizeServerGroupModal.d.ts +43 -0
  50. package/dist/serverGroup/details/resize/index.d.ts +1 -0
  51. package/dist/serverGroup/details/rollback/EcsRollbackServerGroupModal.d.ts +46 -0
  52. package/dist/serverGroup/details/rollback/index.d.ts +1 -0
  53. package/dist/serverGroup/details/sections/EcsServerGroupDetailsSections.d.ts +9 -0
  54. package/dist/serverGroup/details/sections/EcsServerGroupEventsSection.d.ts +3 -0
  55. package/dist/serverGroup/details/sections/EcsServerGroupInformationSection.d.ts +3 -0
  56. package/dist/serverGroup/events/EcsServerGroupEventsModal.d.ts +22 -0
  57. package/dist/serverGroup/events/EventsLink.d.ts +2 -0
  58. package/dist/serverGroup/events/serverGroupEventsReader.service.d.ts +1 -1
  59. package/dist/serverGroup/serverGroup.transformer.d.ts +1 -2
  60. package/dist/serviceDiscovery/serviceDiscovery.read.service.d.ts +1 -1
  61. package/package.json +3 -9
  62. package/src/ecs.module.spec.ts +86 -0
  63. package/src/ecs.module.ts +68 -84
  64. package/src/ecsCluster/ecsCluster.read.service.ts +3 -9
  65. package/src/iamRoles/iamRole.read.service.ts +1 -7
  66. package/src/index.ts +6 -0
  67. package/src/instance/details/EcsInstanceDetails.spec.tsx +426 -0
  68. package/src/instance/details/EcsInstanceDetails.tsx +474 -0
  69. package/src/loadBalancer/details/loadBalancerDetails.tsx +1 -1
  70. package/src/loadBalancer/details/targetGroupDetails.tsx +1 -1
  71. package/src/loadBalancer/loadBalancer.transformer.ts +2 -7
  72. package/src/loadBalancer/targetGroup.states.ts +55 -53
  73. package/src/metricAlarm/metricAlarm.read.service.ts +1 -7
  74. package/src/pipeline/stages/cloneServerGroup/ecsCloneServerGroupStage.js +22 -94
  75. package/src/pipeline/stages/common/EcsServerGroupStepLabel.tsx +16 -0
  76. package/src/pipeline/stages/common/EcsStageConfigs.tsx +587 -0
  77. package/src/pipeline/stages/destroyAsg/ecsDestroyAsgStage.js +29 -65
  78. package/src/pipeline/stages/disableAsg/ecsDisableAsgStage.js +27 -70
  79. package/src/pipeline/stages/disableCluster/ecsDisableClusterStage.js +21 -84
  80. package/src/pipeline/stages/enableAsg/ecsEnableAsgStage.js +22 -73
  81. package/src/pipeline/stages/findImageFromTags/EcsFindImageFromTagsExecutionDetails.tsx +40 -0
  82. package/src/pipeline/stages/findImageFromTags/ecsFindImageFromTagStage.js +12 -24
  83. package/src/pipeline/stages/resizeAsg/ecsResizeAsgStage.js +30 -128
  84. package/src/pipeline/stages/scaleDownCluster/ecsScaleDownClusterStage.js +24 -78
  85. package/src/pipeline/stages/shrinkCluster/ecsShrinkClusterStage.js +19 -73
  86. package/src/placementStrategy/placementStrategy.service.ts +0 -6
  87. package/src/secrets/secret.read.service.ts +1 -7
  88. package/src/securityGroup/details/EcsSecurityGroupDetails.spec.tsx +245 -0
  89. package/src/securityGroup/details/EcsSecurityGroupDetails.tsx +285 -0
  90. package/src/securityGroup/securityGroup.reader.ts +0 -5
  91. package/src/securityGroup/securityGroup.transformer.js +5 -13
  92. package/src/serverGroup/configure/dockerImage.util.ts +29 -0
  93. package/src/serverGroup/configure/serverGroupCommandBuilder.service.js +215 -278
  94. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.ts +83 -0
  95. package/src/serverGroup/configure/serverGroupConfiguration.service.ts +16 -633
  96. package/src/serverGroup/configure/wizard/EcsCloneServerGroupModal.spec.tsx +2078 -0
  97. package/src/serverGroup/configure/wizard/EcsCloneServerGroupModal.tsx +712 -0
  98. package/src/serverGroup/configure/wizard/capacityProvider/CapacityProvider.tsx +98 -49
  99. package/src/serverGroup/configure/wizard/container/Container.spec.tsx +20 -8
  100. package/src/serverGroup/configure/wizard/container/Container.tsx +53 -24
  101. package/src/serverGroup/configure/wizard/networking/Networking.tsx +60 -38
  102. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +290 -0
  103. package/src/serverGroup/configure/wizard/pages/BasicSettings.tsx +101 -0
  104. package/src/serverGroup/configure/wizard/pages/ContainerSettings.tsx +10 -0
  105. package/src/serverGroup/configure/wizard/pages/HorizontalScalingSettings.tsx +137 -0
  106. package/src/serverGroup/configure/wizard/pages/LoggingSettings.tsx +60 -0
  107. package/src/serverGroup/configure/wizard/pages/NetworkingSettings.tsx +10 -0
  108. package/src/serverGroup/configure/wizard/pages/ServiceDiscoverySettings.tsx +10 -0
  109. package/src/serverGroup/configure/wizard/pages/TaskDefinitionSettings.tsx +59 -0
  110. package/src/serverGroup/configure/wizard/pages/common.ts +10 -0
  111. package/src/serverGroup/configure/wizard/pages/validation.tsx +190 -0
  112. package/src/serverGroup/configure/wizard/serviceDiscovery/ServiceDiscovery.tsx +83 -34
  113. package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.spec.tsx +21 -8
  114. package/src/serverGroup/configure/wizard/taskDefinition/TaskDefinition.tsx +63 -33
  115. package/src/serverGroup/details/EcsServerGroupActions.spec.tsx +244 -0
  116. package/src/serverGroup/details/EcsServerGroupActions.tsx +152 -0
  117. package/src/serverGroup/details/ecsServerGroupDetailsGetter.spec.ts +110 -0
  118. package/src/serverGroup/details/ecsServerGroupDetailsGetter.ts +73 -0
  119. package/src/serverGroup/details/resize/EcsResizeServerGroupModal.spec.tsx +130 -0
  120. package/src/serverGroup/details/resize/EcsResizeServerGroupModal.tsx +263 -0
  121. package/src/serverGroup/details/resize/index.ts +1 -0
  122. package/src/serverGroup/details/rollback/EcsRollbackServerGroupModal.spec.tsx +173 -0
  123. package/src/serverGroup/details/rollback/EcsRollbackServerGroupModal.tsx +285 -0
  124. package/src/serverGroup/details/rollback/index.ts +1 -0
  125. package/src/serverGroup/details/sections/EcsServerGroupDetailsSections.spec.tsx +130 -0
  126. package/src/serverGroup/details/sections/EcsServerGroupDetailsSections.tsx +172 -0
  127. package/src/serverGroup/details/sections/EcsServerGroupEventsSection.tsx +14 -0
  128. package/src/serverGroup/details/sections/EcsServerGroupInformationSection.tsx +25 -0
  129. package/src/serverGroup/events/EcsServerGroupEventsModal.spec.tsx +109 -0
  130. package/src/serverGroup/events/EcsServerGroupEventsModal.tsx +112 -0
  131. package/src/serverGroup/events/EventsLink.tsx +18 -0
  132. package/src/serverGroup/events/serverGroupEventsReader.service.spec.ts +41 -0
  133. package/src/serverGroup/events/serverGroupEventsReader.service.ts +2 -8
  134. package/src/serverGroup/serverGroup.transformer.spec.ts +10 -24
  135. package/src/serverGroup/serverGroup.transformer.ts +1 -5
  136. package/src/serviceDiscovery/serviceDiscovery.read.service.ts +1 -1
  137. package/dist/common/common.module.d.ts +0 -1
  138. package/dist/common/footer.component.d.ts +0 -1
  139. package/dist/instance/details/instance.details.controller.d.ts +0 -2
  140. package/dist/securityGroup/details/securityGroupDetail.controller.d.ts +0 -2
  141. package/dist/securityGroup/securityGroup.module.d.ts +0 -1
  142. package/dist/serverGroup/configure/wizard/CloneServerGroup.ecs.controller.d.ts +0 -2
  143. package/dist/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.d.ts +0 -2
  144. package/dist/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.d.ts +0 -2
  145. package/dist/serverGroup/configure/wizard/location/ServerGroupBasicSettings.controller.d.ts +0 -2
  146. package/dist/serverGroup/configure/wizard/logging/logging.component.d.ts +0 -1
  147. package/dist/serverGroup/details/resize/resizeCapacity.component.d.ts +0 -1
  148. package/dist/serverGroup/details/resize/resizeServerGroup.controller.d.ts +0 -2
  149. package/dist/serverGroup/details/rollback/rollbackServerGroup.controller.d.ts +0 -2
  150. package/dist/serverGroup/details/serverGroupDetails.ecs.controller.d.ts +0 -2
  151. package/dist/serverGroup/details/serverGroupDetails.module.d.ts +0 -1
  152. package/dist/serverGroup/events/events.component.d.ts +0 -3
  153. package/dist/serverGroup/events/events.controller.d.ts +0 -19
  154. package/dist/serverGroup/serverGroup.module.d.ts +0 -1
  155. package/src/common/common.module.ts +0 -6
  156. package/src/common/footer.component.ts +0 -29
  157. package/src/instance/details/instance.details.controller.js +0 -356
  158. package/src/instance/details/instanceDetails.html +0 -204
  159. package/src/pipeline/stages/cloneServerGroup/cloneServerGroupStage.html +0 -101
  160. package/src/pipeline/stages/cloneServerGroup/cloneServerGroupStepLabel.html +0 -1
  161. package/src/pipeline/stages/destroyAsg/destroyAsgStage.html +0 -9
  162. package/src/pipeline/stages/destroyAsg/destroyAsgStepLabel.html +0 -1
  163. package/src/pipeline/stages/disableAsg/disableAsgStage.html +0 -11
  164. package/src/pipeline/stages/disableAsg/disableAsgStepLabel.html +0 -1
  165. package/src/pipeline/stages/disableCluster/disableClusterStage.html +0 -26
  166. package/src/pipeline/stages/enableAsg/enableAsgStage.html +0 -11
  167. package/src/pipeline/stages/enableAsg/enableAsgStepLabel.html +0 -1
  168. package/src/pipeline/stages/findImageFromTags/findImageFromTagsExecutionDetails.html +0 -36
  169. package/src/pipeline/stages/findImageFromTags/findImageFromTagsStage.html +0 -9
  170. package/src/pipeline/stages/resizeAsg/resizeAsgStage.html +0 -87
  171. package/src/pipeline/stages/resizeAsg/resizeAsgStepLabel.html +0 -1
  172. package/src/pipeline/stages/scaleDownCluster/scaleDownClusterStage.html +0 -35
  173. package/src/pipeline/stages/shrinkCluster/shrinkClusterStage.html +0 -34
  174. package/src/securityGroup/details/securityGroupDetail.controller.js +0 -147
  175. package/src/securityGroup/details/securityGroupDetail.html +0 -104
  176. package/src/securityGroup/securityGroup.module.ts +0 -12
  177. package/src/serverGroup/configure/wizard/CloneServerGroup.ecs.controller.js +0 -259
  178. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.html +0 -194
  179. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.component.js +0 -17
  180. package/src/serverGroup/configure/wizard/advancedSettings/advancedSettings.html +0 -10
  181. package/src/serverGroup/configure/wizard/capacityProvider/capacityProvider.html +0 -11
  182. package/src/serverGroup/configure/wizard/container/container.html +0 -9
  183. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.html +0 -118
  184. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.component.js +0 -20
  185. package/src/serverGroup/configure/wizard/horizontalScaling/horizontalScaling.html +0 -13
  186. package/src/serverGroup/configure/wizard/location/ServerGroupBasicSettings.controller.js +0 -41
  187. package/src/serverGroup/configure/wizard/location/basicSettings.html +0 -137
  188. package/src/serverGroup/configure/wizard/logging/logging.component.html +0 -29
  189. package/src/serverGroup/configure/wizard/logging/logging.component.ts +0 -12
  190. package/src/serverGroup/configure/wizard/logging/logging.html +0 -7
  191. package/src/serverGroup/configure/wizard/networking/networking.html +0 -9
  192. package/src/serverGroup/configure/wizard/serverGroupWizard.html +0 -55
  193. package/src/serverGroup/configure/wizard/serviceDiscovery/serviceDiscovery.html +0 -11
  194. package/src/serverGroup/configure/wizard/taskDefinition/taskDefinition.html +0 -51
  195. package/src/serverGroup/configure/wizard/templateSelection/templateSelection.html +0 -9
  196. package/src/serverGroup/configure/wizard/verticalScaling/verticalScaling.html +0 -10
  197. package/src/serverGroup/details/resize/resizeCapacity.component.html +0 -94
  198. package/src/serverGroup/details/resize/resizeCapacity.component.ts +0 -14
  199. package/src/serverGroup/details/resize/resizeServerGroup.controller.js +0 -85
  200. package/src/serverGroup/details/resize/resizeServerGroup.html +0 -28
  201. package/src/serverGroup/details/rollback/rollbackServerGroup.controller.js +0 -97
  202. package/src/serverGroup/details/rollback/rollbackServerGroup.html +0 -67
  203. package/src/serverGroup/details/serverGroupDetails.ecs.controller.js +0 -366
  204. package/src/serverGroup/details/serverGroupDetails.html +0 -216
  205. package/src/serverGroup/details/serverGroupDetails.module.ts +0 -5
  206. package/src/serverGroup/events/events.component.ts +0 -35
  207. package/src/serverGroup/events/events.controller.ts +0 -45
  208. package/src/serverGroup/events/events.html +0 -36
  209. package/src/serverGroup/serverGroup.module.ts +0 -6
@@ -0,0 +1,2078 @@
1
+ import type { ShallowWrapper } from 'enzyme';
2
+ import { mount, shallow } from 'enzyme';
3
+ import React from 'react';
4
+
5
+ import {
6
+ AccountService,
7
+ AccountSelectInput,
8
+ DeploymentStrategySelector,
9
+ RegionSelectInput,
10
+ RequestBuilder,
11
+ TetheredSelect,
12
+ MapEditor,
13
+ WizardModal,
14
+ WizardPage,
15
+ } from '@spinnaker/core';
16
+
17
+ import type { IEcsServerGroupCommand } from '../serverGroupConfiguration.service';
18
+ import { ServiceDiscoveryReader } from '../../../serviceDiscovery/serviceDiscovery.read.service';
19
+ import { EcsCloneServerGroupModalComponent as EcsCloneServerGroupModal } from './EcsCloneServerGroupModal';
20
+ import { BasicSettings } from './pages/BasicSettings';
21
+ import { NetworkingSettings } from './pages/NetworkingSettings';
22
+ import { EcsNetworking } from './networking/Networking';
23
+ import { TaskDefinitionSettings } from './pages/TaskDefinitionSettings';
24
+ import { TaskDefinition } from './taskDefinition/TaskDefinition';
25
+ import { ContainerSettings } from './pages/ContainerSettings';
26
+ import { Container } from './container/Container';
27
+ import { HorizontalScalingSettings } from './pages/HorizontalScalingSettings';
28
+ import { EcsCapacityProvider } from './capacityProvider/CapacityProvider';
29
+ import { LoggingSettings } from './pages/LoggingSettings';
30
+ import { ServiceDiscoverySettings } from './pages/ServiceDiscoverySettings';
31
+ import { ServiceDiscovery } from './serviceDiscovery/ServiceDiscovery';
32
+ import { AdvancedSettings } from './pages/AdvancedSettings';
33
+
34
+ const buildCommand = (overrides: Partial<IEcsServerGroupCommand> = {}): IEcsServerGroupCommand =>
35
+ ({
36
+ backingData: {
37
+ filtered: {
38
+ availableCapacityProviders: [],
39
+ defaultCapacityProviderStrategy: [],
40
+ ecsClusters: [],
41
+ },
42
+ launchTypes: ['EC2', 'FARGATE'],
43
+ },
44
+ capacityProviderStrategy: [],
45
+ containerMappings: [],
46
+ targetGroupMappings: [],
47
+ taskDefinitionArtifact: {},
48
+ useDefaultCapacityProviders: true,
49
+ viewState: { contextImages: [], dirty: {} },
50
+ ...overrides,
51
+ } as any);
52
+
53
+ const buildProps = (command: IEcsServerGroupCommand) =>
54
+ ({
55
+ application: {
56
+ name: 'app',
57
+ serverGroups: {
58
+ onNextRefresh: jasmine.createSpy('onNextRefresh'),
59
+ refresh: jasmine.createSpy('refresh'),
60
+ },
61
+ },
62
+ closeModal: jasmine.createSpy('closeModal'),
63
+ command,
64
+ dismissModal: jasmine.createSpy('dismissModal'),
65
+ title: 'Deploy ECS server group',
66
+ } as any);
67
+
68
+ const buildUnrenderedModal = (command = buildCommand()): EcsCloneServerGroupModal => {
69
+ const modal = new EcsCloneServerGroupModal(buildProps(command));
70
+ spyOn(modal, 'setState').and.callFake((state: any, callback?: () => void) => {
71
+ modal.state = { ...modal.state, ...state };
72
+ callback?.();
73
+ });
74
+ return modal;
75
+ };
76
+
77
+ const findByTestId = (wrapper: ShallowWrapper, testId: string): ShallowWrapper =>
78
+ wrapper.findWhere((node) => node.prop('data-test-id') === testId);
79
+
80
+ const renderCapacityProvider = (command: IEcsServerGroupCommand): ShallowWrapper => {
81
+ const wrapper = shallow(
82
+ <EcsCapacityProvider
83
+ command={command}
84
+ configureCommand={() => Promise.resolve()}
85
+ onFieldChange={(field, value) => (command[field] = value)}
86
+ />,
87
+ { disableLifecycleMethods: true } as any,
88
+ );
89
+ wrapper.setState({ capacityProviderLoadedFlag: true });
90
+ return wrapper;
91
+ };
92
+
93
+ describe('EcsCloneServerGroupModal', () => {
94
+ it('does not set state when backing data resolves after unmount', async () => {
95
+ const modal = new EcsCloneServerGroupModal(buildProps(buildCommand())) as any;
96
+ let resolveBackingData: () => void;
97
+ spyOn(modal, 'loadBackingData').and.returnValue(
98
+ new Promise<void>((resolve) => {
99
+ resolveBackingData = resolve;
100
+ }),
101
+ );
102
+ const setState = spyOn(modal, 'setState');
103
+
104
+ const configurePromise = modal.configureCommand();
105
+ modal.componentWillUnmount();
106
+ resolveBackingData();
107
+ await configurePromise;
108
+
109
+ expect(setState).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it('ignores stale backing data requests', async () => {
113
+ const firstCommand = buildCommand({ ecsClusterName: 'first-cluster' });
114
+ const secondCommand = buildCommand({ ecsClusterName: 'second-cluster' });
115
+ const modal = new EcsCloneServerGroupModal(buildProps(firstCommand)) as any;
116
+ let resolveFirst: () => void;
117
+ let resolveSecond: () => void;
118
+ spyOn(modal, 'loadBackingData').and.callFake((command: IEcsServerGroupCommand) => {
119
+ return new Promise<void>((resolve) => {
120
+ if (command === firstCommand) {
121
+ resolveFirst = resolve;
122
+ } else {
123
+ resolveSecond = resolve;
124
+ }
125
+ });
126
+ });
127
+ spyOn(modal, 'setState').and.callFake((state: any) => {
128
+ modal.state = { ...modal.state, ...state };
129
+ });
130
+
131
+ const firstConfigure = modal.configureCommand();
132
+ modal.state = { ...modal.state, command: secondCommand };
133
+ const secondConfigure = modal.configureCommand();
134
+
135
+ resolveSecond();
136
+ await secondConfigure;
137
+ expect(modal.state.command).toBe(secondCommand);
138
+
139
+ resolveFirst();
140
+ await firstConfigure;
141
+ expect(modal.state.command).toBe(secondCommand);
142
+ });
143
+
144
+ it('does not apply backing data from stale requests', async () => {
145
+ const originalHttpClient = RequestBuilder.defaultHttpClient;
146
+ const command = buildCommand({ backingData: { filtered: {} } as any });
147
+ const modal = new EcsCloneServerGroupModal(buildProps(command)) as any;
148
+ spyOn(AccountService, 'getCredentialsKeyedByAccount').and.returnValue(Promise.resolve({}));
149
+ RequestBuilder.defaultHttpClient = {
150
+ get: (config: any) => {
151
+ if (config.url.endsWith('loadBalancers')) {
152
+ return Promise.resolve([{ name: 'stale-load-balancer' }]);
153
+ }
154
+ if (config.url.endsWith('subnets/ecs')) {
155
+ return Promise.resolve([]);
156
+ }
157
+ if (config.url.endsWith('securityGroups')) {
158
+ return Promise.resolve({});
159
+ }
160
+ return Promise.resolve([]);
161
+ },
162
+ } as any;
163
+ spyOn(modal.iamRoleReader, 'listRoles').and.returnValue(Promise.resolve([]));
164
+ spyOn(modal.ecsClusterReader, 'listClusters').and.returnValue(Promise.resolve([]));
165
+ spyOn(modal.ecsClusterReader, 'describeClusters').and.returnValue(Promise.resolve([]));
166
+ spyOn(modal.metricAlarmReader, 'listMetricAlarms').and.returnValue(Promise.resolve([]));
167
+ spyOn(modal.secretReader, 'listSecrets').and.returnValue(Promise.resolve([]));
168
+
169
+ modal.configureRequest = 2;
170
+ await modal.loadBackingData(command, '', 1);
171
+ RequestBuilder.defaultHttpClient = originalHttpClient;
172
+
173
+ expect(command.backingData.loadBalancers).toBeUndefined();
174
+ });
175
+
176
+ it('does not replace edits made while configureCommand is pending', async () => {
177
+ const command = buildCommand({ capacity: { desired: 1, max: 2, min: 0 } });
178
+ const modal = buildUnrenderedModal(command) as any;
179
+ let resolveBackingData: () => void;
180
+ spyOn(modal, 'loadBackingData').and.returnValue(
181
+ new Promise<void>((resolve) => {
182
+ resolveBackingData = resolve;
183
+ }),
184
+ );
185
+ const formik = {
186
+ setFieldValue: jasmine.createSpy('setFieldValue'),
187
+ setValues: jasmine.createSpy('setValues'),
188
+ values: command,
189
+ } as any;
190
+
191
+ const configurePromise = modal.configureCommand();
192
+ modal.updateCommand(formik, 'capacity', { desired: 3, max: 4, min: 0 });
193
+ resolveBackingData();
194
+ await configurePromise;
195
+
196
+ expect(modal.state.command.capacity).toEqual({ desired: 3, max: 4, min: 0 });
197
+ });
198
+
199
+ it('merges refreshed backing data into edits made while configureCommand is pending', async () => {
200
+ const command = buildCommand({
201
+ backingData: {
202
+ filtered: {
203
+ availableCapacityProviders: ['old-provider'],
204
+ defaultCapacityProviderStrategy: [],
205
+ ecsClusters: [],
206
+ images: [],
207
+ targetGroups: ['old-target'],
208
+ },
209
+ } as any,
210
+ capacity: { desired: 1, max: 2, min: 0 },
211
+ stack: 'old-stack',
212
+ });
213
+ const modal = buildUnrenderedModal(command) as any;
214
+ modal.attachEventHandlers(command);
215
+ let resolveBackingData: () => void;
216
+ spyOn(modal, 'loadBackingData').and.callFake(
217
+ (requestCommand: IEcsServerGroupCommand) =>
218
+ new Promise<void>((resolve) => {
219
+ resolveBackingData = () => {
220
+ const images = [{ imageId: 'registry/refreshed:latest' }];
221
+ requestCommand.backingData = {
222
+ ...requestCommand.backingData,
223
+ filtered: {
224
+ ...requestCommand.backingData.filtered,
225
+ availableCapacityProviders: ['refreshed-provider'],
226
+ images,
227
+ targetGroups: ['refreshed-target'],
228
+ },
229
+ images,
230
+ } as any;
231
+ resolve();
232
+ };
233
+ }),
234
+ );
235
+ const formik = {
236
+ setFieldValue: jasmine.createSpy('setFieldValue'),
237
+ setValues: jasmine.createSpy('setValues'),
238
+ values: command,
239
+ } as any;
240
+
241
+ const configurePromise = modal.configureCommand('refreshed');
242
+ modal.updateCommand(formik, 'stack', 'edited-stack');
243
+ modal.updateCommand(formik, 'capacity', { desired: 3, max: 4, min: 0 });
244
+ resolveBackingData();
245
+ await configurePromise;
246
+
247
+ expect(modal.state.command.stack).toBe('edited-stack');
248
+ expect(modal.state.command.capacity).toEqual({ desired: 3, max: 4, min: 0 });
249
+ expect(modal.state.command.backingData.filtered.targetGroups).toEqual(['refreshed-target']);
250
+ expect(modal.state.command.backingData.filtered.images).toEqual([{ imageId: 'registry/refreshed:latest' }]);
251
+ expect(modal.state.command.backingData.filtered.availableCapacityProviders).toEqual(['refreshed-provider']);
252
+ });
253
+
254
+ it('ignores an in-flight response and refreshes options when subnet types change', async () => {
255
+ const command = buildCommand({
256
+ backingData: {
257
+ filtered: {
258
+ availableCapacityProviders: [],
259
+ defaultCapacityProviderStrategy: [],
260
+ ecsClusters: [],
261
+ securityGroupNames: ['initial-security-group'],
262
+ subnetTypes: [{ purpose: 'old-subnet', vpcId: 'old-vpc' }],
263
+ },
264
+ } as any,
265
+ subnetTypes: ['old-subnet'],
266
+ });
267
+ const modal = buildUnrenderedModal(command) as any;
268
+ modal.attachEventHandlers(command);
269
+ const requests: Array<{
270
+ command: IEcsServerGroupCommand;
271
+ promise: Promise<void>;
272
+ resolve: () => void;
273
+ }> = [];
274
+ const loadBackingData = spyOn(modal, 'loadBackingData').and.callFake((requestCommand: IEcsServerGroupCommand) => {
275
+ let resolve: () => void;
276
+ const promise = new Promise<void>((requestResolve) => {
277
+ resolve = requestResolve;
278
+ });
279
+ requests.push({ command: requestCommand, promise, resolve });
280
+ return promise;
281
+ });
282
+ const formik = {
283
+ setFieldValue: jasmine.createSpy('setFieldValue'),
284
+ setValues: jasmine.createSpy('setValues'),
285
+ values: command,
286
+ } as any;
287
+
288
+ const oldConfigure = modal.configureCommand();
289
+ modal.updateCommand(formik, 'subnetTypes', ['new-subnet']);
290
+
291
+ expect(loadBackingData).toHaveBeenCalledTimes(2);
292
+ const newRequest = requests[1];
293
+ if (newRequest) {
294
+ newRequest.command.backingData = {
295
+ ...newRequest.command.backingData,
296
+ filtered: {
297
+ ...newRequest.command.backingData.filtered,
298
+ securityGroupNames: ['new-security-group'],
299
+ subnetTypes: [{ purpose: 'new-subnet', vpcId: 'new-vpc' }],
300
+ },
301
+ } as any;
302
+ newRequest.resolve();
303
+ await newRequest.promise;
304
+ }
305
+
306
+ const oldRequest = requests[0];
307
+ oldRequest.command.backingData = {
308
+ ...oldRequest.command.backingData,
309
+ filtered: {
310
+ ...oldRequest.command.backingData.filtered,
311
+ securityGroupNames: ['old-security-group'],
312
+ subnetTypes: [{ purpose: 'old-subnet', vpcId: 'old-vpc' }],
313
+ },
314
+ } as any;
315
+ oldRequest.resolve();
316
+ await oldConfigure;
317
+
318
+ expect(modal.state.command.subnetTypes).toEqual(['new-subnet']);
319
+ expect(modal.state.command.backingData.filtered.securityGroupNames).toEqual(['new-security-group']);
320
+ expect(modal.state.command.backingData.filtered.subnetTypes).toEqual([{ purpose: 'new-subnet', vpcId: 'new-vpc' }]);
321
+ });
322
+
323
+ it('preserves image options while refreshing security groups after a subnet change', async () => {
324
+ const originalHttpClient = RequestBuilder.defaultHttpClient;
325
+ const images = [
326
+ { id: 'backing-api-v1', imageId: 'registry/api:v1' },
327
+ { id: 'backing-api-v2', imageId: 'registry/api:v2' },
328
+ ];
329
+ const subnets = [
330
+ { account: 'ecs-account', purpose: 'old-subnet', region: 'us-west-2', vpcId: 'old-vpc' },
331
+ { account: 'ecs-account', purpose: 'new-subnet', region: 'us-west-2', vpcId: 'new-vpc' },
332
+ ];
333
+ const securityGroups = {
334
+ 'ecs-account': {
335
+ ecs: {
336
+ 'us-west-2': [
337
+ { name: 'old-security-group', vpcId: 'old-vpc' },
338
+ { name: 'new-security-group', vpcId: 'new-vpc' },
339
+ ],
340
+ },
341
+ },
342
+ };
343
+ const command = buildCommand({
344
+ backingData: {
345
+ filtered: {
346
+ availableCapacityProviders: [],
347
+ defaultCapacityProviderStrategy: [],
348
+ ecsClusters: [],
349
+ images,
350
+ securityGroupNames: ['old-security-group'],
351
+ },
352
+ images,
353
+ } as any,
354
+ containerMappings: [
355
+ {
356
+ containerName: 'worker',
357
+ imageDescription: { id: 'mapped-api-v2', imageId: 'registry/api:v2' },
358
+ } as any,
359
+ ],
360
+ credentials: 'ecs-account',
361
+ imageDescription: { id: 'selected-api-v1', imageId: 'registry/api:v1' } as any,
362
+ region: 'us-west-2',
363
+ subnetTypes: ['old-subnet'],
364
+ });
365
+ const modal = buildUnrenderedModal(command) as any;
366
+ modal.attachEventHandlers(command);
367
+ const securityGroupRequests: Array<{
368
+ promise: Promise<any>;
369
+ resolve: (value: any) => void;
370
+ }> = [];
371
+ RequestBuilder.defaultHttpClient = {
372
+ get: (config: any) => {
373
+ if (config.url.endsWith('securityGroups')) {
374
+ let resolve: (value: any) => void;
375
+ const promise = new Promise<any>((requestResolve) => {
376
+ resolve = requestResolve;
377
+ });
378
+ securityGroupRequests.push({ promise, resolve });
379
+ return promise;
380
+ }
381
+ if (config.url.endsWith('subnets/ecs')) {
382
+ return Promise.resolve(subnets);
383
+ }
384
+ return Promise.resolve([]);
385
+ },
386
+ } as any;
387
+ spyOn(AccountService, 'getCredentialsKeyedByAccount').and.returnValue(
388
+ Promise.resolve({
389
+ 'ecs-account': { regions: [{ availabilityZones: ['us-west-2a'], name: 'us-west-2' }] },
390
+ }) as any,
391
+ );
392
+ spyOn(modal.iamRoleReader, 'listRoles').and.returnValue(Promise.resolve([]));
393
+ spyOn(modal.ecsClusterReader, 'listClusters').and.returnValue(Promise.resolve([]));
394
+ spyOn(modal.ecsClusterReader, 'describeClusters').and.returnValue(Promise.resolve([]));
395
+ spyOn(modal.metricAlarmReader, 'listMetricAlarms').and.returnValue(Promise.resolve([]));
396
+ spyOn(modal.secretReader, 'listSecrets').and.returnValue(Promise.resolve([]));
397
+ spyOn(ServiceDiscoveryReader, 'listServiceDiscoveryRegistries').and.returnValue(Promise.resolve([]));
398
+ const configureCommand = spyOn(modal, 'configureCommand').and.callThrough();
399
+ const formik = {
400
+ setFieldValue: jasmine.createSpy('setFieldValue'),
401
+ setValues: jasmine.createSpy('setValues'),
402
+ values: command,
403
+ } as any;
404
+
405
+ try {
406
+ const oldConfigure = modal.configureCommand();
407
+ modal.updateCommand(formik, 'subnetTypes', ['new-subnet']);
408
+ const newConfigure = configureCommand.calls.mostRecent().returnValue;
409
+
410
+ securityGroupRequests[1].resolve(securityGroups);
411
+ await newConfigure;
412
+ securityGroupRequests[0].resolve(securityGroups);
413
+ await oldConfigure;
414
+
415
+ expect(modal.state.command.backingData.filtered.securityGroupNames).toEqual(['new-security-group']);
416
+ expect(modal.state.command.backingData.filtered.images.map((image: any) => image.imageId)).toEqual([
417
+ 'registry/api:v1',
418
+ 'registry/api:v2',
419
+ ]);
420
+
421
+ modal.updateCommand(formik, 'subnetTypes', ['old-subnet']);
422
+ const repeatedConfigure = configureCommand.calls.mostRecent().returnValue;
423
+ securityGroupRequests[2].resolve(securityGroups);
424
+ await repeatedConfigure;
425
+
426
+ expect(modal.state.command.backingData.filtered.securityGroupNames).toEqual(['old-security-group']);
427
+ expect(modal.state.command.backingData.filtered.images.map((image: any) => image.imageId)).toEqual([
428
+ 'registry/api:v1',
429
+ 'registry/api:v2',
430
+ ]);
431
+ } finally {
432
+ RequestBuilder.defaultHttpClient = originalHttpClient;
433
+ }
434
+ });
435
+
436
+ it('initializes optional backing data arrays used by child sections', () => {
437
+ const command = buildCommand({ backingData: { filtered: {} } as any });
438
+ const modal = buildUnrenderedModal(command) as any;
439
+
440
+ modal.ensureCommandShape(command);
441
+
442
+ expect(command.backingData.filtered.iamRoles).toEqual([]);
443
+ expect(command.backingData.filtered.metricAlarms).toEqual([]);
444
+ expect(command.backingData.filtered.secrets).toEqual([]);
445
+ expect(command.backingData.filtered.serviceDiscoveryRegistries).toEqual([]);
446
+ });
447
+
448
+ it('populates regions and availability zones for the selected account during initial configuration', async () => {
449
+ const originalHttpClient = RequestBuilder.defaultHttpClient;
450
+ const regions = [
451
+ { availabilityZones: ['eu-west-1a', 'eu-west-1b'], name: 'eu-west-1' },
452
+ { availabilityZones: ['us-east-1a'], name: 'us-east-1' },
453
+ ];
454
+ const command = buildCommand({
455
+ backingData: { filtered: {} } as any,
456
+ credentials: 'ecs-account',
457
+ region: 'eu-west-1',
458
+ });
459
+ const modal = new EcsCloneServerGroupModal(buildProps(command)) as any;
460
+ spyOn(AccountService, 'getCredentialsKeyedByAccount').and.returnValue(
461
+ Promise.resolve({ 'ecs-account': { regions } }) as any,
462
+ );
463
+ RequestBuilder.defaultHttpClient = {
464
+ get: (config: any) => Promise.resolve(config.url.endsWith('securityGroups') ? {} : []),
465
+ } as any;
466
+ spyOn(modal.iamRoleReader, 'listRoles').and.returnValue(Promise.resolve([]));
467
+ spyOn(modal.ecsClusterReader, 'listClusters').and.returnValue(Promise.resolve([]));
468
+ spyOn(modal.ecsClusterReader, 'describeClusters').and.returnValue(Promise.resolve([]));
469
+ spyOn(modal.metricAlarmReader, 'listMetricAlarms').and.returnValue(Promise.resolve([]));
470
+ spyOn(modal.secretReader, 'listSecrets').and.returnValue(Promise.resolve([]));
471
+ spyOn(ServiceDiscoveryReader, 'listServiceDiscoveryRegistries').and.returnValue(Promise.resolve([]));
472
+
473
+ await modal.loadBackingData(command, '');
474
+ RequestBuilder.defaultHttpClient = originalHttpClient;
475
+
476
+ expect(command.backingData.filtered.regions).toEqual(regions);
477
+ expect(command.backingData.filtered.availabilityZones).toEqual(['eu-west-1a', 'eu-west-1b']);
478
+ expect(command.availabilityZones).toEqual(['eu-west-1a', 'eu-west-1b']);
479
+ });
480
+
481
+ it('reconciles invalid location selections before account and region handlers reload backing data', () => {
482
+ const command = buildCommand({
483
+ backingData: {
484
+ credentialsKeyedByAccount: {
485
+ 'account-b': { regions: [{ availabilityZones: ['us-east-1a'], name: 'us-east-1' }] },
486
+ },
487
+ filtered: { regions: [{ availabilityZones: ['eu-west-1a'], name: 'eu-west-1' }] },
488
+ } as any,
489
+ credentials: 'account-b',
490
+ region: 'eu-west-1',
491
+ });
492
+ const modal = buildUnrenderedModal(command) as any;
493
+ const configureCommand = spyOn(modal, 'configureCommand').and.returnValue(Promise.resolve());
494
+
495
+ modal.attachEventHandlers(command);
496
+ command.credentialsChanged(command);
497
+
498
+ expect(command.backingData.filtered.regions).toEqual([{ availabilityZones: ['us-east-1a'], name: 'us-east-1' }]);
499
+ expect(command.region).toBeNull();
500
+ expect(configureCommand).toHaveBeenCalledTimes(1);
501
+
502
+ command.region = 'not-an-account-region';
503
+ command.regionChanged(command);
504
+
505
+ expect(command.region).toBeNull();
506
+ expect(configureCommand).toHaveBeenCalledTimes(2);
507
+ });
508
+
509
+ it('filters security groups when the subnet type changes', () => {
510
+ const command = buildCommand({
511
+ backingData: {
512
+ ...buildCommand().backingData,
513
+ securityGroups: {
514
+ 'ecs-account': {
515
+ ecs: {
516
+ 'us-west-2': [
517
+ { name: 'private-sg', vpcId: 'vpc-1' },
518
+ { name: 'public-sg', vpcId: 'vpc-2' },
519
+ ],
520
+ },
521
+ },
522
+ },
523
+ subnets: [
524
+ { account: 'ecs-account', purpose: 'private-subnet', region: 'us-west-2', vpcId: 'vpc-1' },
525
+ { account: 'ecs-account', purpose: 'public-subnet', region: 'us-west-2', vpcId: 'vpc-2' },
526
+ ],
527
+ } as any,
528
+ credentials: 'ecs-account',
529
+ region: 'us-west-2',
530
+ subnetTypes: ['private-subnet'],
531
+ });
532
+ const modal = buildUnrenderedModal(command) as any;
533
+
534
+ modal.attachEventHandlers(command);
535
+ command.subnetTypeChanged(command);
536
+
537
+ expect(command.backingData.filtered.securityGroupNames).toEqual(['private-sg']);
538
+ });
539
+
540
+ it('reloads backing data when account or region changes', () => {
541
+ const command = buildCommand();
542
+ const modal = buildUnrenderedModal(command) as any;
543
+ const configureCommand = spyOn(modal, 'configureCommand').and.returnValue(Promise.resolve());
544
+
545
+ modal.attachEventHandlers(command);
546
+ command.credentialsChanged(command);
547
+ command.regionChanged(command);
548
+
549
+ expect(configureCommand).toHaveBeenCalledTimes(2);
550
+ });
551
+
552
+ ['createPipeline', 'editPipeline'].forEach((mode) => {
553
+ it(`returns the transformed command without executing infrastructure in ${mode} mode`, () => {
554
+ const command = buildCommand({ viewState: { contextImages: [], dirty: {}, mode } as any });
555
+ const modal = buildUnrenderedModal(command) as any;
556
+ const cloneServerGroup = jasmine.createSpy('cloneServerGroup');
557
+
558
+ modal.submit();
559
+
560
+ expect(modal.props.closeModal).toHaveBeenCalledWith(command);
561
+ expect(cloneServerGroup).not.toHaveBeenCalled();
562
+ });
563
+ });
564
+
565
+ it('uses authoritative Formik values for account normalization, display, and submission', () => {
566
+ const regions = [{ availabilityZones: ['us-east-1a'], name: 'us-east-1' }];
567
+ const command = buildCommand({
568
+ backingData: {
569
+ credentialsKeyedByAccount: {
570
+ 'account-a': { regions: [{ availabilityZones: ['eu-west-1a'], name: 'eu-west-1' }] },
571
+ 'account-b': { regions },
572
+ },
573
+ filtered: { regions: [{ availabilityZones: ['eu-west-1a'], name: 'eu-west-1' }] },
574
+ } as any,
575
+ credentials: 'account-a',
576
+ region: 'eu-west-1',
577
+ viewState: { contextImages: [], dirty: {}, mode: 'editPipeline' } as any,
578
+ });
579
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
580
+ disableLifecycleMethods: true,
581
+ } as any);
582
+ const modal = wrapper.instance() as any;
583
+ spyOn(modal, 'configureCommand').and.returnValue(Promise.resolve());
584
+ modal.attachEventHandlers(command);
585
+ const formik: any = {
586
+ setFieldValue: jasmine.createSpy('setFieldValue').and.callFake((field: string, value: any) => {
587
+ formik.values = { ...formik.values, [field]: value };
588
+ }),
589
+ setValues: jasmine.createSpy('setValues').and.callFake((values: IEcsServerGroupCommand) => {
590
+ formik.values = values;
591
+ }),
592
+ values: { ...command },
593
+ };
594
+ const wizardModal = wrapper.find(WizardModal);
595
+ const renderPages = () =>
596
+ shallow(
597
+ <div>
598
+ {wizardModal.prop('render')({
599
+ formik,
600
+ nextIdx: (() => {
601
+ let index = 0;
602
+ return () => ++index;
603
+ })(),
604
+ wizard: {} as any,
605
+ })}
606
+ </div>,
607
+ );
608
+
609
+ const basicPage = renderPages()
610
+ .find(WizardPage)
611
+ .filterWhere((page) => page.prop('label') === 'Basic Settings');
612
+ const basicSettings = basicPage.prop('render')({
613
+ innerRef: React.createRef(),
614
+ onLoadingChanged: () => undefined,
615
+ }).props.children;
616
+ basicSettings.props.onFieldChange('credentials', 'account-b');
617
+
618
+ const displayedCommand = renderPages()
619
+ .find(WizardPage)
620
+ .filterWhere((page) => page.prop('label') === 'Basic Settings')
621
+ .prop('render')({ innerRef: React.createRef(), onLoadingChanged: () => undefined }).props.children.props.command;
622
+ expect(formik.setValues).toHaveBeenCalled();
623
+ expect(displayedCommand.credentials).toBe('account-b');
624
+ expect(displayedCommand.region).toBeNull();
625
+ expect(displayedCommand.backingData.filtered.regions).toEqual(regions);
626
+
627
+ wizardModal.prop('closeModal')(formik.values);
628
+ expect(wrapper.instance().props.closeModal).toHaveBeenCalledWith(displayedCommand);
629
+ });
630
+
631
+ it('uses authoritative Formik values across artifact and input mode normalization and submission', () => {
632
+ const command = buildCommand({
633
+ serviceDiscoveryAssociations: [
634
+ {
635
+ containerName: 'api',
636
+ containerPort: 8080,
637
+ registry: { displayName: 'registry', id: 'registry' },
638
+ } as any,
639
+ ],
640
+ useTaskDefinitionArtifact: true,
641
+ viewState: { contextImages: [], dirty: {}, mode: 'editPipeline' } as any,
642
+ });
643
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
644
+ disableLifecycleMethods: true,
645
+ } as any);
646
+ const formik: any = {
647
+ setFieldValue: jasmine.createSpy('setFieldValue').and.callFake((field: string, value: any) => {
648
+ formik.values = { ...formik.values, [field]: value };
649
+ }),
650
+ setValues: jasmine.createSpy('setValues').and.callFake((values: IEcsServerGroupCommand) => {
651
+ formik.values = values;
652
+ }),
653
+ values: { ...command },
654
+ };
655
+ const wizardModal = wrapper.find(WizardModal);
656
+ const renderPages = () =>
657
+ shallow(
658
+ <div>
659
+ {wizardModal.prop('render')({
660
+ formik,
661
+ nextIdx: (() => {
662
+ let index = 0;
663
+ return () => ++index;
664
+ })(),
665
+ wizard: {} as any,
666
+ })}
667
+ </div>,
668
+ );
669
+
670
+ let taskDefinition = renderPages()
671
+ .find(WizardPage)
672
+ .filterWhere((page) => page.prop('label') === 'Task Definition')
673
+ .prop('render')({ innerRef: React.createRef(), onLoadingChanged: () => undefined }).props.children;
674
+ taskDefinition.props.onFieldChange('useTaskDefinitionArtifact', false);
675
+ expect(formik.values.serviceDiscoveryAssociations[0].containerName).toBeNull();
676
+ expect(
677
+ renderPages()
678
+ .find(WizardPage)
679
+ .someWhere((page) => page.prop('label') === 'Container'),
680
+ ).toBe(true);
681
+
682
+ taskDefinition = renderPages()
683
+ .find(WizardPage)
684
+ .filterWhere((page) => page.prop('label') === 'Task Definition')
685
+ .prop('render')({ innerRef: React.createRef(), onLoadingChanged: () => undefined }).props.children;
686
+ taskDefinition.props.onFieldChange('useTaskDefinitionArtifact', true);
687
+ expect(formik.values.serviceDiscoveryAssociations[0].containerName).toBe('');
688
+ expect(
689
+ renderPages()
690
+ .find(WizardPage)
691
+ .someWhere((page) => page.prop('label') === 'Container'),
692
+ ).toBe(false);
693
+ expect(formik.setValues).toHaveBeenCalledTimes(2);
694
+
695
+ wizardModal.prop('closeModal')(formik.values);
696
+ expect(wrapper.instance().props.closeModal).toHaveBeenCalledWith(formik.values);
697
+ });
698
+
699
+ it('composes sequential updates before asynchronous Formik values settle', () => {
700
+ const command = buildCommand({ preferSourceCapacity: false, useSourceCapacity: false });
701
+ const modal = buildUnrenderedModal(command) as any;
702
+ const formik = {
703
+ setFieldValue: jasmine.createSpy('setFieldValue'),
704
+ setValues: jasmine.createSpy('setValues'),
705
+ values: { ...command },
706
+ } as any;
707
+
708
+ modal.updateCommand(formik, 'useSourceCapacity', true);
709
+ modal.updateCommand(formik, 'preferSourceCapacity', true);
710
+
711
+ expect(modal.state.command.useSourceCapacity).toBe(true);
712
+ expect(modal.state.command.preferSourceCapacity).toBe(true);
713
+ expect(formik.setValues.calls.mostRecent().args[0]).toEqual(modal.state.command);
714
+ });
715
+
716
+ ['create', 'clone'].forEach((mode) => {
717
+ it(`submits ad-hoc ${mode} commands through TaskMonitor`, () => {
718
+ const command = buildCommand({ viewState: { contextImages: [], dirty: {}, mode } as any });
719
+ const props = buildProps(command);
720
+ const modal = new EcsCloneServerGroupModal(props) as any;
721
+ const task = Promise.resolve({ id: 'task-id' });
722
+ const cloneServerGroup = jasmine.createSpy('cloneServerGroup').and.returnValue(task as any);
723
+ modal.context = { services: { serverGroupWriter: { cloneServerGroup } } };
724
+ const monitorSubmit = spyOn(
725
+ modal.state.taskMonitor,
726
+ 'submit',
727
+ ).and.callFake((submitMethod: () => PromiseLike<any>) => submitMethod());
728
+
729
+ modal.submit();
730
+
731
+ expect(monitorSubmit).toHaveBeenCalled();
732
+ expect(cloneServerGroup).toHaveBeenCalledWith(modal.state.command, props.application);
733
+ expect(props.closeModal).not.toHaveBeenCalled();
734
+ });
735
+ });
736
+
737
+ it('refreshes server groups and navigates to the created group from task and command identity', () => {
738
+ const command = buildCommand({
739
+ credentials: 'ecs-account',
740
+ region: 'eu-west-1',
741
+ viewState: { contextImages: [], dirty: {}, mode: 'create' } as any,
742
+ });
743
+ const props = buildProps(command);
744
+ const modal = new EcsCloneServerGroupModal(props) as any;
745
+ modal.state.taskMonitor.task = {
746
+ execution: {
747
+ stages: [
748
+ {
749
+ context: { 'deploy.server.groups': { 'eu-west-1': 'app-main-v042' } },
750
+ type: 'cloneServerGroup',
751
+ },
752
+ ],
753
+ },
754
+ };
755
+ const state = {
756
+ go: jasmine.createSpy('go'),
757
+ includes: jasmine.createSpy('includes').and.callFake((name: string) => name === '**.clusters'),
758
+ };
759
+ props.stateService = state;
760
+
761
+ modal.onTaskComplete();
762
+
763
+ expect(props.application.serverGroups.refresh).toHaveBeenCalled();
764
+ expect(props.application.serverGroups.onNextRefresh).toHaveBeenCalledWith(modal.onApplicationRefresh);
765
+
766
+ const refreshCallback = props.application.serverGroups.onNextRefresh.calls.mostRecent().args[0];
767
+ refreshCallback();
768
+
769
+ expect(state.includes).toHaveBeenCalledWith('**.clusters');
770
+ expect(state.go).toHaveBeenCalledWith('.serverGroup', {
771
+ accountId: 'ecs-account',
772
+ provider: 'ecs',
773
+ region: 'eu-west-1',
774
+ serverGroup: 'app-main-v042',
775
+ });
776
+ });
777
+
778
+ it('owns its refresh subscription across replacement and unmount', () => {
779
+ const props = buildProps(buildCommand());
780
+ const firstUnsubscribe = jasmine.createSpy('firstUnsubscribe');
781
+ const secondUnsubscribe = jasmine.createSpy('secondUnsubscribe');
782
+ const callbacks: Array<() => void> = [];
783
+ props.application.serverGroups.onNextRefresh.and.callFake((callback: () => void) => {
784
+ callbacks.push(callback);
785
+ return callbacks.length === 1 ? firstUnsubscribe : secondUnsubscribe;
786
+ });
787
+ props.stateService = { go: jasmine.createSpy('go'), includes: jasmine.createSpy('includes') };
788
+ const modal = new EcsCloneServerGroupModal(props) as any;
789
+
790
+ modal.onTaskComplete();
791
+
792
+ expect(props.application.serverGroups.onNextRefresh.calls.first().invocationOrder).toBeLessThan(
793
+ props.application.serverGroups.refresh.calls.first().invocationOrder,
794
+ );
795
+
796
+ modal.onTaskComplete();
797
+
798
+ expect(firstUnsubscribe).toHaveBeenCalledTimes(1);
799
+
800
+ modal.componentWillUnmount();
801
+ callbacks[1]();
802
+
803
+ expect(secondUnsubscribe).toHaveBeenCalledTimes(1);
804
+ expect(modal.applicationRefreshUnsubscribe).toBeUndefined();
805
+ expect(props.stateService.go).not.toHaveBeenCalled();
806
+ });
807
+
808
+ it('retains modal command state when ad-hoc submission fails', async () => {
809
+ const command = buildCommand({ viewState: { contextImages: [], dirty: {}, mode: 'create' } as any });
810
+ const props = buildProps(command);
811
+ const modal = new EcsCloneServerGroupModal(props) as any;
812
+ const cloneServerGroup = jasmine
813
+ .createSpy('cloneServerGroup')
814
+ .and.callFake(() => Promise.reject({ failureMessage: 'create failed' }) as any);
815
+ modal.context = { services: { serverGroupWriter: { cloneServerGroup } } };
816
+ const errorPublished = new Promise<void>((resolve) => {
817
+ const subscription = modal.state.taskMonitor.statusUpdatedStream.subscribe(() => {
818
+ if (modal.state.taskMonitor.error) {
819
+ subscription.unsubscribe();
820
+ resolve();
821
+ }
822
+ });
823
+ });
824
+
825
+ modal.submit();
826
+ await errorPublished;
827
+
828
+ expect(modal.state.taskMonitor.error).toBe(true);
829
+ expect(modal.state.taskMonitor.errorMessage).toBe('create failed');
830
+ expect(modal.state.command).toBe(command);
831
+ expect(props.closeModal).not.toHaveBeenCalled();
832
+ expect(props.dismissModal).not.toHaveBeenCalled();
833
+ });
834
+
835
+ it('renders task submission and failure status inside the modal', () => {
836
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(buildCommand())} />, {
837
+ disableLifecycleMethods: true,
838
+ } as any);
839
+
840
+ expect(wrapper.find(WizardModal).prop('taskMonitor')).toBe(wrapper.state('taskMonitor'));
841
+ });
842
+
843
+ it('renders the legacy eight-page WizardModal grouping', () => {
844
+ const command = buildCommand({ useTaskDefinitionArtifact: false });
845
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
846
+ disableLifecycleMethods: true,
847
+ } as any);
848
+ wrapper.setState({ loaded: true });
849
+
850
+ const wizardModal = wrapper.find(WizardModal);
851
+ expect(wizardModal.exists()).toBe(true);
852
+
853
+ const pageTree = shallow(
854
+ <div>
855
+ {wizardModal.prop('render')({
856
+ formik: { values: command } as any,
857
+ nextIdx: (() => {
858
+ let index = 0;
859
+ return () => ++index;
860
+ })(),
861
+ wizard: {} as any,
862
+ })}
863
+ </div>,
864
+ );
865
+
866
+ expect(pageTree.find(WizardPage).map((page) => page.prop('label'))).toEqual([
867
+ 'Basic Settings',
868
+ 'Networking',
869
+ 'Task Definition',
870
+ 'Container',
871
+ 'Horizontal Scaling',
872
+ 'Logging',
873
+ 'Service Discovery',
874
+ 'Advanced Settings',
875
+ ]);
876
+ });
877
+
878
+ it('validates required location, task source, and populated mapping fields', () => {
879
+ const command = buildCommand({
880
+ capacity: { desired: 1, max: 2, min: 0 },
881
+ credentials: '',
882
+ ecsClusterName: '',
883
+ freeFormDetails: '',
884
+ region: '',
885
+ stack: '',
886
+ taskDefinitionArtifact: {},
887
+ useTaskDefinitionArtifact: true,
888
+ });
889
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
890
+ disableLifecycleMethods: true,
891
+ } as any);
892
+ const validate = wrapper.find(WizardModal).prop('validate');
893
+
894
+ const requiredErrors = validate(command);
895
+ expect(requiredErrors.credentials).toBeTruthy();
896
+ expect(requiredErrors.region).toBeTruthy();
897
+ expect(requiredErrors.ecsClusterName).toBeTruthy();
898
+ expect(requiredErrors.taskDefinitionArtifact).toBeTruthy();
899
+
900
+ const artifactMappingErrors = validate({
901
+ ...command,
902
+ containerMappings: [{ containerName: '', imageDescription: {} }],
903
+ credentials: 'account',
904
+ ecsClusterName: 'cluster',
905
+ region: 'eu-west-1',
906
+ serviceDiscoveryAssociations: [{ containerName: '', containerPort: NaN, registry: {} }],
907
+ targetGroupMappings: [{ containerName: '', containerPort: NaN, targetGroup: '' }],
908
+ taskDefinitionArtifact: { artifactId: 'artifact-id' },
909
+ } as any);
910
+ expect(artifactMappingErrors.containerMappings[0].containerName).toBeTruthy();
911
+ expect(artifactMappingErrors.containerMappings[0].imageDescription).toBeTruthy();
912
+ expect(artifactMappingErrors.targetGroupMappings[0].containerName).toBeTruthy();
913
+ expect(artifactMappingErrors.targetGroupMappings[0].targetGroup).toBeTruthy();
914
+ expect(artifactMappingErrors.targetGroupMappings[0].containerPort).toBeTruthy();
915
+ expect(artifactMappingErrors.serviceDiscoveryAssociations[0].containerName).toBeTruthy();
916
+ expect(artifactMappingErrors.serviceDiscoveryAssociations[0].registry).toBeTruthy();
917
+ expect(artifactMappingErrors.serviceDiscoveryAssociations[0].containerPort).toBeTruthy();
918
+
919
+ const inputErrors = validate({
920
+ ...command,
921
+ credentials: 'account',
922
+ ecsClusterName: 'cluster',
923
+ imageDescription: {},
924
+ region: 'eu-west-1',
925
+ serviceDiscoveryAssociations: [{ containerName: null, containerPort: NaN, registry: {} }],
926
+ targetGroupMappings: [{ containerName: null, containerPort: NaN, targetGroup: '' }],
927
+ useTaskDefinitionArtifact: false,
928
+ } as any);
929
+ expect(inputErrors.imageDescription.imageId).toBeTruthy();
930
+ expect(inputErrors.targetGroupMappings[0].containerName).toBeUndefined();
931
+ expect(inputErrors.targetGroupMappings[0].targetGroup).toBeTruthy();
932
+ expect(inputErrors.serviceDiscoveryAssociations[0].containerName).toBeUndefined();
933
+ });
934
+
935
+ it('validates naming patterns and finite ordered capacity through page refs', () => {
936
+ const command = buildCommand({
937
+ capacity: { desired: Infinity, max: 1.5, min: NaN },
938
+ credentials: 'account',
939
+ ecsClusterName: 'cluster',
940
+ freeFormDetails: 'detail!',
941
+ region: 'eu-west-1',
942
+ stack: 'main-stack',
943
+ taskDefinitionArtifact: { artifactId: 'artifact-id' },
944
+ useTaskDefinitionArtifact: true,
945
+ });
946
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
947
+ disableLifecycleMethods: true,
948
+ } as any);
949
+ const wizardModal = wrapper.find(WizardModal);
950
+ const validate = wizardModal.prop('validate');
951
+ const errors = validate(command);
952
+ expect(errors.stack).toBeTruthy();
953
+ expect(errors.freeFormDetails).toBeTruthy();
954
+ expect(errors.capacity.min).toBeTruthy();
955
+ expect(errors.capacity.desired).toBeTruthy();
956
+ expect(errors.capacity.max).toBeTruthy();
957
+
958
+ const validCommand = {
959
+ ...command,
960
+ capacity: { desired: 2, max: 3, min: 1 },
961
+ freeFormDetails: 'detail-one_1.0',
962
+ launchType: 'FARGATE',
963
+ stack: 'main_stack.1',
964
+ } as IEcsServerGroupCommand;
965
+ expect(validate(validCommand)).toEqual({});
966
+
967
+ const relationshipErrors = validate({ ...validCommand, capacity: { desired: 4, max: 3, min: 2 } });
968
+ expect(relationshipErrors.capacity.desired).toBeTruthy();
969
+ const reversedErrors = validate({ ...validCommand, capacity: { desired: 2, max: 1, min: 3 } });
970
+ expect(reversedErrors.capacity.min).toBeTruthy();
971
+
972
+ const pageTree = shallow(
973
+ <div>
974
+ {wizardModal.prop('render')({
975
+ formik: { values: validCommand } as any,
976
+ nextIdx: (() => {
977
+ let index = 0;
978
+ return () => ++index;
979
+ })(),
980
+ wizard: {
981
+ onWizardPageAdded: () => undefined,
982
+ onWizardPageRemoved: () => undefined,
983
+ onWizardPageStateChanged: () => undefined,
984
+ },
985
+ })}
986
+ </div>,
987
+ );
988
+ const capacityPage = pageTree
989
+ .find(WizardPage)
990
+ .filterWhere((page) => page.prop('label') === 'Horizontal Scaling')
991
+ .getElement();
992
+ const mountedPage = mount(capacityPage);
993
+ const pageErrors = (mountedPage.instance() as WizardPage<IEcsServerGroupCommand>).validate(command);
994
+ expect(pageErrors.capacity.min).toBeTruthy();
995
+ expect(pageErrors.capacity.desired).toBeTruthy();
996
+ expect(pageErrors.capacity.max).toBeTruthy();
997
+ mountedPage.unmount();
998
+ });
999
+
1000
+ it('requires a launch type in launch type compute mode', () => {
1001
+ const command = buildCommand({
1002
+ capacity: { desired: 1, max: 2, min: 0 },
1003
+ computeOption: 'launchType',
1004
+ credentials: 'account',
1005
+ ecsClusterName: 'cluster',
1006
+ launchType: '',
1007
+ region: 'eu-west-1',
1008
+ taskDefinitionArtifact: { artifactId: 'artifact-id' },
1009
+ useTaskDefinitionArtifact: true,
1010
+ });
1011
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
1012
+ disableLifecycleMethods: true,
1013
+ } as any);
1014
+ const validate = wrapper.find(WizardModal).prop('validate');
1015
+
1016
+ expect(validate(command).launchType).toBeTruthy();
1017
+ expect(validate({ ...command, launchType: 'FARGATE' }).launchType).toBeUndefined();
1018
+ });
1019
+
1020
+ it('requires a usable default strategy in default capacity provider mode', () => {
1021
+ const command = buildCommand({
1022
+ backingData: {
1023
+ ...buildCommand().backingData,
1024
+ filtered: { ...buildCommand().backingData.filtered, defaultCapacityProviderStrategy: [] },
1025
+ },
1026
+ capacity: { desired: 1, max: 2, min: 0 },
1027
+ computeOption: 'capacityProviders',
1028
+ credentials: 'account',
1029
+ ecsClusterName: 'cluster',
1030
+ region: 'eu-west-1',
1031
+ taskDefinitionArtifact: { artifactId: 'artifact-id' },
1032
+ useDefaultCapacityProviders: true,
1033
+ useTaskDefinitionArtifact: true,
1034
+ });
1035
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
1036
+ disableLifecycleMethods: true,
1037
+ } as any);
1038
+ const validate = wrapper.find(WizardModal).prop('validate');
1039
+
1040
+ expect(validate(command).capacityProviderStrategy).toBeTruthy();
1041
+ expect(
1042
+ validate({
1043
+ ...command,
1044
+ backingData: {
1045
+ ...command.backingData,
1046
+ filtered: {
1047
+ ...command.backingData.filtered,
1048
+ defaultCapacityProviderStrategy: [{ base: -1, capacityProvider: '', weight: NaN }],
1049
+ },
1050
+ },
1051
+ }).capacityProviderStrategy,
1052
+ ).toBeTruthy();
1053
+ expect(
1054
+ validate({
1055
+ ...command,
1056
+ backingData: {
1057
+ ...command.backingData,
1058
+ filtered: {
1059
+ ...command.backingData.filtered,
1060
+ defaultCapacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
1061
+ },
1062
+ },
1063
+ }).capacityProviderStrategy,
1064
+ ).toBeUndefined();
1065
+ });
1066
+
1067
+ it('validates every custom capacity provider strategy row', () => {
1068
+ const command = buildCommand({
1069
+ capacity: { desired: 1, max: 2, min: 0 },
1070
+ capacityProviderStrategy: [],
1071
+ computeOption: 'capacityProviders',
1072
+ credentials: 'account',
1073
+ ecsClusterName: 'cluster',
1074
+ region: 'eu-west-1',
1075
+ taskDefinitionArtifact: { artifactId: 'artifact-id' },
1076
+ useDefaultCapacityProviders: false,
1077
+ useTaskDefinitionArtifact: true,
1078
+ });
1079
+ const wrapper = shallow(<EcsCloneServerGroupModal {...buildProps(command)} />, {
1080
+ disableLifecycleMethods: true,
1081
+ } as any);
1082
+ const validate = wrapper.find(WizardModal).prop('validate');
1083
+
1084
+ expect(validate(command).capacityProviderStrategy).toBeTruthy();
1085
+
1086
+ const errors = validate({
1087
+ ...command,
1088
+ capacityProviderStrategy: [
1089
+ { base: -1, capacityProvider: '', weight: Infinity },
1090
+ { base: 1.5, capacityProvider: 'FARGATE', weight: NaN },
1091
+ ],
1092
+ } as any).capacityProviderStrategy;
1093
+ expect(errors[0].capacityProvider).toBeTruthy();
1094
+ expect(errors[0].base).toBeTruthy();
1095
+ expect(errors[0].weight).toBeTruthy();
1096
+ expect(errors[1].capacityProvider).toBeUndefined();
1097
+ expect(errors[1].base).toBeTruthy();
1098
+ expect(errors[1].weight).toBeTruthy();
1099
+
1100
+ expect(
1101
+ validate({
1102
+ ...command,
1103
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
1104
+ }).capacityProviderStrategy,
1105
+ ).toBeUndefined();
1106
+ });
1107
+
1108
+ it('does not submit the wizard from add, remove, or option buttons', () => {
1109
+ const command = buildCommand({
1110
+ backingData: {
1111
+ filtered: {
1112
+ availableCapacityProviders: ['FARGATE'],
1113
+ defaultCapacityProviderStrategy: [],
1114
+ images: [],
1115
+ serviceDiscoveryRegistries: [{ displayName: 'registry', id: 'registry' }],
1116
+ targetGroups: ['target-group'],
1117
+ },
1118
+ networkModes: [],
1119
+ } as any,
1120
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
1121
+ credentials: 'account',
1122
+ ecsClusterName: 'cluster',
1123
+ placementConstraints: [{}],
1124
+ region: 'eu-west-1',
1125
+ serviceDiscoveryAssociations: [
1126
+ { containerName: 'api', containerPort: 8080, registry: { displayName: 'registry', id: 'registry' } } as any,
1127
+ ],
1128
+ targetGroupMappings: [{ containerName: 'api', containerPort: 8080, targetGroup: 'target-group' }],
1129
+ useDefaultCapacityProviders: false,
1130
+ useTaskDefinitionArtifact: true,
1131
+ viewState: { contextImages: [], currentStage: {}, dirty: {}, pipeline: {} } as any,
1132
+ });
1133
+ const childProps = {
1134
+ command,
1135
+ configureCommand: () => Promise.resolve(),
1136
+ onFieldChange: jasmine.createSpy('onFieldChange'),
1137
+ };
1138
+ const capacityProvider = shallow(<EcsCapacityProvider {...childProps} />, {
1139
+ disableLifecycleMethods: true,
1140
+ } as any);
1141
+ capacityProvider.setState({ activeCapacityProviderIndex: 0, capacityProviderLoadedFlag: true });
1142
+ const wrappers = [
1143
+ shallow(<Container {...childProps} />, { disableLifecycleMethods: true } as any),
1144
+ shallow(<TaskDefinition {...childProps} />, { disableLifecycleMethods: true } as any),
1145
+ shallow(<ServiceDiscovery {...childProps} />, { disableLifecycleMethods: true } as any),
1146
+ capacityProvider,
1147
+ shallow(
1148
+ <AdvancedSettings
1149
+ application={buildProps(command).application}
1150
+ command={command}
1151
+ configureCommand={() => Promise.resolve()}
1152
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1153
+ />,
1154
+ ),
1155
+ ];
1156
+
1157
+ const buttons = wrappers.flatMap((wrapper) => wrapper.find('button').getElements());
1158
+ expect(buttons.length).toBeGreaterThan(0);
1159
+ buttons.forEach((button) => expect(button.props.type).toBe('button'));
1160
+ });
1161
+
1162
+ it('restores Basic Settings account, region, cluster, naming, and strategy controls', () => {
1163
+ const command = buildCommand({
1164
+ backingData: {
1165
+ accounts: ['account-a', 'account-b'],
1166
+ filtered: {
1167
+ ecsClusters: ['available-cluster', 'persisted-cluster'],
1168
+ regions: [{ name: 'eu-west-1' }, { name: 'us-east-1' }],
1169
+ },
1170
+ } as any,
1171
+ credentials: 'account-a',
1172
+ ecsClusterName: 'persisted-cluster',
1173
+ freeFormDetails: 'api',
1174
+ region: 'eu-west-1',
1175
+ selectedProvider: 'ecs',
1176
+ stack: 'prod',
1177
+ viewState: { contextImages: [], dirty: {}, disableStrategySelection: false } as any,
1178
+ });
1179
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1180
+ const wrapper = shallow(
1181
+ React.createElement(BasicSettings as any, {
1182
+ application: buildProps(command).application,
1183
+ command,
1184
+ onFieldChange,
1185
+ }),
1186
+ );
1187
+
1188
+ expect(wrapper.find(AccountSelectInput).prop('value')).toBe('account-a');
1189
+ expect(wrapper.find(RegionSelectInput).prop('value')).toBe('eu-west-1');
1190
+ expect(findByTestId(wrapper, 'ServerGroup.clusterName').prop('value')).toBe('persisted-cluster');
1191
+ expect(findByTestId(wrapper, 'ServerGroup.stack').prop('value')).toBe('prod');
1192
+ expect(findByTestId(wrapper, 'ServerGroup.details').prop('value')).toBe('api');
1193
+ expect(wrapper.find(DeploymentStrategySelector).exists()).toBe(true);
1194
+
1195
+ wrapper.find(AccountSelectInput).prop('onChange')({ target: { value: 'account-b' } } as any);
1196
+ wrapper.find(RegionSelectInput).prop('onChange')({ target: { value: 'us-east-1' } } as any);
1197
+ findByTestId(wrapper, 'ServerGroup.clusterName').simulate('change', { target: { value: 'available-cluster' } });
1198
+
1199
+ expect(onFieldChange).toHaveBeenCalledWith('credentials', 'account-b');
1200
+ expect(onFieldChange).toHaveBeenCalledWith('region', 'us-east-1');
1201
+ expect(onFieldChange).toHaveBeenCalledWith('ecsClusterName', 'available-cluster');
1202
+ });
1203
+
1204
+ it('restores Networking VPC subnet and security-group controls without hiding persisted references', () => {
1205
+ const command = buildCommand({
1206
+ associatePublicIpAddress: false,
1207
+ backingData: {
1208
+ filtered: {
1209
+ securityGroupNames: ['available-sg'],
1210
+ subnetTypes: [{ purpose: 'available-subnet', vpcId: 'vpc-1' }],
1211
+ },
1212
+ networkModes: ['bridge', 'awsvpc'],
1213
+ } as any,
1214
+ networkMode: 'awsvpc',
1215
+ securityGroupNames: ['persisted-sg'],
1216
+ subnetTypes: ['persisted-subnet'],
1217
+ });
1218
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1219
+ const configureCommand = jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve());
1220
+ const page = shallow(
1221
+ React.createElement(NetworkingSettings as any, {
1222
+ application: buildProps(command).application,
1223
+ command,
1224
+ configureCommand,
1225
+ onFieldChange,
1226
+ }),
1227
+ );
1228
+
1229
+ expect(page.find(EcsNetworking).exists()).toBe(true);
1230
+ const networking = shallow(page.find(EcsNetworking).getElement());
1231
+ const subnetOptions = findByTestId(networking, 'Networking.subnetType').find(TetheredSelect).prop('options');
1232
+ const securityGroupOptions = findByTestId(networking, 'Networking.securityGroups')
1233
+ .find(TetheredSelect)
1234
+ .prop('options');
1235
+
1236
+ expect(subnetOptions.map((option: any) => option.value)).toEqual(['available-subnet', 'persisted-subnet']);
1237
+ expect(securityGroupOptions.map((option: any) => option.value)).toEqual(['available-sg', 'persisted-sg']);
1238
+ });
1239
+
1240
+ it('restores Task Definition source, artifact mappings, and persisted target groups', () => {
1241
+ const command = buildCommand({
1242
+ backingData: {
1243
+ filtered: {
1244
+ images: [],
1245
+ targetGroups: ['available-target'],
1246
+ },
1247
+ } as any,
1248
+ containerMappings: [],
1249
+ targetGroupMappings: [{ containerName: 'api', containerPort: 8080, targetGroup: 'persisted-target' }],
1250
+ taskDefinitionArtifact: {},
1251
+ useTaskDefinitionArtifact: true,
1252
+ viewState: {
1253
+ contextImages: [],
1254
+ currentStage: {},
1255
+ dirty: {},
1256
+ mode: 'editPipeline',
1257
+ pipeline: {},
1258
+ } as any,
1259
+ });
1260
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1261
+ const configureCommand = jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve());
1262
+ const page = shallow(
1263
+ React.createElement(TaskDefinitionSettings as any, {
1264
+ application: buildProps(command).application,
1265
+ command,
1266
+ configureCommand,
1267
+ onFieldChange,
1268
+ }),
1269
+ );
1270
+
1271
+ expect(findByTestId(page, 'ServerGroup.useInputs').prop('checked')).toBe(false);
1272
+ expect(findByTestId(page, 'ServerGroup.useArtifacts').prop('checked')).toBe(true);
1273
+ expect(page.find(TaskDefinition).exists()).toBe(true);
1274
+
1275
+ findByTestId(page, 'ServerGroup.useInputs').simulate('change');
1276
+ expect(onFieldChange).toHaveBeenCalledWith('useTaskDefinitionArtifact', false);
1277
+
1278
+ const taskDefinition = shallow(page.find(TaskDefinition).getElement());
1279
+ const targetOptions = findByTestId(taskDefinition, 'Artifacts.targetGroup').find(TetheredSelect).prop('options');
1280
+ expect(targetOptions.map((option: any) => option.value)).toEqual(['available-target', 'persisted-target']);
1281
+ });
1282
+
1283
+ it('restores Container image, resources, and persisted target-group mappings', () => {
1284
+ const command = buildCommand({
1285
+ backingData: {
1286
+ filtered: {
1287
+ images: [{ imageId: 'registry/api:latest' }],
1288
+ targetGroups: ['available-target'],
1289
+ },
1290
+ } as any,
1291
+ computeUnits: 512,
1292
+ imageDescription: { imageId: 'registry/api:latest' } as any,
1293
+ reservedMemory: 1024,
1294
+ targetGroupMappings: [{ containerName: '', containerPort: 8080, targetGroup: 'persisted-target' }],
1295
+ });
1296
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1297
+ const page = shallow(
1298
+ React.createElement(ContainerSettings as any, {
1299
+ application: buildProps(command).application,
1300
+ command,
1301
+ configureCommand: jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve()),
1302
+ onFieldChange,
1303
+ }),
1304
+ );
1305
+
1306
+ expect(page.find(Container).exists()).toBe(true);
1307
+ const container = shallow(page.find(Container).getElement());
1308
+ expect(findByTestId(container, 'ContainerInputs.containerImage').exists()).toBe(true);
1309
+ expect(findByTestId(container, 'ContainerInputs.computeUnits').prop('value')).toBe(512);
1310
+ expect(findByTestId(container, 'ContainerInputs.reservedMemory').prop('value')).toBe(1024);
1311
+ const targetOptions = findByTestId(container, 'ContainerInputs.targetGroup').find(TetheredSelect).prop('options');
1312
+ expect(targetOptions.map((option: any) => option.value)).toEqual(['available-target', 'persisted-target']);
1313
+ });
1314
+
1315
+ it('restores Horizontal Scaling compute mode, capacity, and source-policy controls', () => {
1316
+ const command = buildCommand({
1317
+ capacity: { desired: 3, max: 5, min: 2 },
1318
+ computeOption: 'launchType',
1319
+ copySourceScalingPoliciesAndActions: true,
1320
+ launchType: 'FARGATE',
1321
+ useSourceCapacity: false,
1322
+ });
1323
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1324
+ const page = shallow(
1325
+ React.createElement(HorizontalScalingSettings as any, {
1326
+ application: buildProps(command).application,
1327
+ command,
1328
+ configureCommand: jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve()),
1329
+ onFieldChange,
1330
+ }),
1331
+ );
1332
+
1333
+ expect(findByTestId(page, 'ServerGroup.computeOptionsLaunchType').prop('checked')).toBe(true);
1334
+ expect(findByTestId(page, 'ServerGroup.launchType').prop('value')).toBe('FARGATE');
1335
+ expect(findByTestId(page, 'ServerGroup.capacity.desired').prop('value')).toBe(3);
1336
+ expect(findByTestId(page, 'ServerGroup.capacity.min').prop('value')).toBe(2);
1337
+ expect(findByTestId(page, 'ServerGroup.capacity.max').prop('value')).toBe(5);
1338
+ expect(findByTestId(page, 'ServerGroup.useSourceCapacity').prop('checked')).toBe(false);
1339
+ expect(findByTestId(page, 'ServerGroup.copySourceScalingPoliciesAndActions').prop('checked')).toBe(true);
1340
+
1341
+ findByTestId(page, 'ServerGroup.computeOptionsCapacityProviders').simulate('change');
1342
+ expect(onFieldChange).toHaveBeenCalledWith('computeOption', 'capacityProviders');
1343
+ expect(onFieldChange).toHaveBeenCalledWith('launchType', '');
1344
+
1345
+ const capacityProviderPage = shallow(
1346
+ React.createElement(HorizontalScalingSettings as any, {
1347
+ application: buildProps(command).application,
1348
+ command: { ...command, computeOption: 'capacityProviders' },
1349
+ configureCommand: jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve()),
1350
+ onFieldChange,
1351
+ }),
1352
+ );
1353
+ expect(capacityProviderPage.find(EcsCapacityProvider).exists()).toBe(true);
1354
+ });
1355
+
1356
+ it('restores Logging driver and option-map controls', () => {
1357
+ const command = buildCommand({ logDriver: 'awslogs', logOptions: { 'awslogs-region': 'eu-west-1' } });
1358
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1359
+ const page = shallow(
1360
+ React.createElement(LoggingSettings as any, {
1361
+ application: buildProps(command).application,
1362
+ command,
1363
+ onFieldChange,
1364
+ }),
1365
+ );
1366
+
1367
+ expect(findByTestId(page, 'Logging.logDriver').prop('value')).toBe('awslogs');
1368
+ expect(page.find(MapEditor).prop('model')).toEqual({ 'awslogs-region': 'eu-west-1' });
1369
+
1370
+ findByTestId(page, 'Logging.logDriver').simulate('change', { target: { value: 'fluentd' } });
1371
+ page.find(MapEditor).prop('onChange')({ address: 'localhost:24224' }, true);
1372
+ expect(onFieldChange).toHaveBeenCalledWith('logDriver', 'fluentd');
1373
+ expect(onFieldChange).toHaveBeenCalledWith('logOptions', { address: 'localhost:24224' });
1374
+ });
1375
+
1376
+ it('restores Service Discovery mappings without hiding persisted registries', () => {
1377
+ const availableRegistry = { displayName: 'available (registry-1)', id: 'registry-1' };
1378
+ const persistedRegistry = { displayName: 'persisted (registry-2)', id: 'registry-2' };
1379
+ const command = buildCommand({
1380
+ backingData: { filtered: { serviceDiscoveryRegistries: [availableRegistry] } } as any,
1381
+ serviceDiscoveryAssociations: [{ containerName: 'api', containerPort: 8080, registry: persistedRegistry } as any],
1382
+ useTaskDefinitionArtifact: true,
1383
+ });
1384
+ const page = shallow(
1385
+ React.createElement(ServiceDiscoverySettings as any, {
1386
+ application: buildProps(command).application,
1387
+ command,
1388
+ configureCommand: jasmine.createSpy('configureCommand').and.returnValue(Promise.resolve()),
1389
+ onFieldChange: jasmine.createSpy('onFieldChange'),
1390
+ }),
1391
+ );
1392
+
1393
+ expect(page.find(ServiceDiscovery).exists()).toBe(true);
1394
+ const serviceDiscovery = shallow(page.find(ServiceDiscovery).getElement());
1395
+ const registryOptions = findByTestId(serviceDiscovery, 'ServiceDiscovery.registry')
1396
+ .find(TetheredSelect)
1397
+ .prop('options');
1398
+ expect(registryOptions.map((option: any) => option.value)).toEqual([
1399
+ 'available (registry-1)',
1400
+ 'persisted (registry-2)',
1401
+ ]);
1402
+ expect(findByTestId(serviceDiscovery, 'ServiceDiscovery.containerName').prop('value')).toBe('api');
1403
+ expect(findByTestId(serviceDiscovery, 'ServiceDiscovery.containerPort').prop('value')).toBe(8080);
1404
+ });
1405
+
1406
+ it('refreshes Networking options from rerendered command props without clobbering selections', () => {
1407
+ const command = buildCommand({
1408
+ associatePublicIpAddress: false,
1409
+ backingData: {
1410
+ filtered: {
1411
+ securityGroupNames: ['old-sg'],
1412
+ subnetTypes: [{ purpose: 'old-subnet', vpcId: 'vpc-old' }],
1413
+ },
1414
+ networkModes: ['awsvpc'],
1415
+ } as any,
1416
+ networkMode: 'awsvpc',
1417
+ securityGroupNames: ['persisted-sg'],
1418
+ subnetTypes: ['persisted-subnet'],
1419
+ });
1420
+ const wrapper = shallow(
1421
+ <EcsNetworking
1422
+ command={command}
1423
+ configureCommand={() => new Promise<void>(() => undefined)}
1424
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1425
+ />,
1426
+ );
1427
+
1428
+ command.backingData.filtered.securityGroupNames = ['new-sg'];
1429
+ command.backingData.filtered.subnetTypes = [{ purpose: 'new-subnet', vpcId: 'vpc-new' } as any];
1430
+ wrapper.setProps({ command });
1431
+
1432
+ expect(wrapper.state('securityGroupNames')).toEqual(['persisted-sg']);
1433
+ expect(wrapper.state('subnetTypes')).toEqual(['persisted-subnet']);
1434
+ expect(wrapper.state('securityGroupsAvailable')).toEqual(['new-sg', 'persisted-sg']);
1435
+ expect((wrapper.state('subnetTypesAvailable') as any[]).map((subnet) => subnet.purpose)).toEqual([
1436
+ 'new-subnet',
1437
+ 'persisted-subnet',
1438
+ ]);
1439
+ });
1440
+
1441
+ it('refreshes Container options from rerendered command props without clobbering selections', () => {
1442
+ const persistedImage = { imageId: 'registry/persisted:latest' } as any;
1443
+ const command = buildCommand({
1444
+ backingData: { filtered: { images: [], targetGroups: ['old-target'] } } as any,
1445
+ imageDescription: persistedImage,
1446
+ targetGroupMappings: [{ containerName: '', containerPort: 8080, targetGroup: 'persisted-target' }],
1447
+ });
1448
+ const wrapper = shallow(
1449
+ <Container
1450
+ command={command}
1451
+ configureCommand={() => new Promise<void>(() => undefined)}
1452
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1453
+ />,
1454
+ );
1455
+
1456
+ command.backingData.filtered.images = [{ imageId: 'registry/new:latest' } as any];
1457
+ command.backingData.filtered.targetGroups = ['new-target'];
1458
+ wrapper.setProps({ command });
1459
+
1460
+ expect(wrapper.state('imageDescription')).toBe(persistedImage);
1461
+ expect(wrapper.state('targetGroupMappings')).toEqual(command.targetGroupMappings);
1462
+ expect((wrapper.state('dockerImages') as any[]).map((image) => image.imageId)).toEqual(['registry/new:latest']);
1463
+ expect(wrapper.state('targetGroupsAvailable')).toEqual(['new-target', 'persisted-target']);
1464
+ });
1465
+
1466
+ it('refreshes TaskDefinition options from rerendered command props without clobbering selections', () => {
1467
+ const persistedArtifact = { artifactId: 'expected-artifact' };
1468
+ const command = buildCommand({
1469
+ backingData: { filtered: { images: [], targetGroups: ['old-target'] } } as any,
1470
+ containerMappings: [{ containerName: 'api', imageDescription: { imageId: 'registry/persisted:latest' } as any }],
1471
+ targetGroupMappings: [{ containerName: 'api', containerPort: 8080, targetGroup: 'persisted-target' }],
1472
+ taskDefinitionArtifact: persistedArtifact,
1473
+ });
1474
+ const wrapper = shallow(
1475
+ <TaskDefinition
1476
+ command={command}
1477
+ configureCommand={() => new Promise<void>(() => undefined)}
1478
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1479
+ />,
1480
+ );
1481
+
1482
+ command.backingData.filtered.images = [{ imageId: 'registry/new:latest' } as any];
1483
+ command.backingData.filtered.targetGroups = ['new-target'];
1484
+ wrapper.setProps({ command });
1485
+
1486
+ expect(wrapper.state('taskDefArtifact')).toBe(persistedArtifact);
1487
+ expect(wrapper.state('containerMappings')).toEqual(command.containerMappings);
1488
+ expect(wrapper.state('targetGroupMappings')).toEqual(command.targetGroupMappings);
1489
+ expect((wrapper.state('dockerImages') as any[]).map((image) => image.imageId)).toEqual(['registry/new:latest']);
1490
+ expect(wrapper.state('targetGroupsAvailable')).toEqual(['new-target', 'persisted-target']);
1491
+ });
1492
+
1493
+ it('refreshes ServiceDiscovery options from rerendered command props without clobbering selections', () => {
1494
+ const persistedRegistry = { displayName: 'persisted (registry-1)', id: 'registry-1' };
1495
+ const associations = [{ containerName: 'api', containerPort: 8080, registry: persistedRegistry } as any];
1496
+ const command = buildCommand({
1497
+ backingData: {
1498
+ filtered: { serviceDiscoveryRegistries: [{ displayName: 'old (registry-2)', id: 'registry-2' }] },
1499
+ } as any,
1500
+ serviceDiscoveryAssociations: associations,
1501
+ useTaskDefinitionArtifact: true,
1502
+ });
1503
+ const wrapper = shallow(
1504
+ <ServiceDiscovery
1505
+ command={command}
1506
+ configureCommand={() => new Promise<void>(() => undefined)}
1507
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1508
+ />,
1509
+ );
1510
+
1511
+ command.backingData.filtered.serviceDiscoveryRegistries = [
1512
+ { displayName: 'new (registry-3)', id: 'registry-3' } as any,
1513
+ ];
1514
+ wrapper.setProps({ command });
1515
+
1516
+ expect(wrapper.state('serviceDiscoveryAssociations')).toBe(associations);
1517
+ expect(
1518
+ (wrapper.state('serviceDiscoveryRegistriesAvailable') as any[]).map((registry) => registry.displayName),
1519
+ ).toEqual(['new (registry-3)', 'persisted (registry-1)']);
1520
+ });
1521
+
1522
+ it('normalizes ServiceDiscovery container names when switching away from task definition artifacts', () => {
1523
+ const registry = { displayName: 'persisted (registry-1)', id: 'registry-1' };
1524
+ const command = buildCommand({
1525
+ serviceDiscoveryAssociations: [{ containerName: 'api', containerPort: 8080, registry } as any],
1526
+ useTaskDefinitionArtifact: true,
1527
+ });
1528
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1529
+ const wrapper = shallow(
1530
+ <ServiceDiscovery
1531
+ command={command}
1532
+ configureCommand={() => new Promise<void>(() => undefined)}
1533
+ onFieldChange={onFieldChange}
1534
+ />,
1535
+ );
1536
+
1537
+ command.useTaskDefinitionArtifact = false;
1538
+ wrapper.setProps({ command });
1539
+
1540
+ expect(wrapper.state('useTaskDefinitionArtifact')).toBe(false);
1541
+ expect(wrapper.state('serviceDiscoveryAssociations')).toEqual([
1542
+ { containerName: null, containerPort: 8080, registry } as any,
1543
+ ]);
1544
+ expect(findByTestId(wrapper, 'ServiceDiscovery.containerName').exists()).toBe(false);
1545
+ expect(onFieldChange).toHaveBeenCalledWith(
1546
+ 'serviceDiscoveryAssociations',
1547
+ wrapper.state('serviceDiscoveryAssociations'),
1548
+ );
1549
+ });
1550
+
1551
+ it('preserves and normalizes ServiceDiscovery container names when switching to task definition artifacts', () => {
1552
+ const registry = { displayName: 'persisted (registry-1)', id: 'registry-1' };
1553
+ const command = buildCommand({
1554
+ serviceDiscoveryAssociations: [{ containerName: null, containerPort: 8080, registry } as any],
1555
+ useTaskDefinitionArtifact: false,
1556
+ });
1557
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1558
+ const wrapper = shallow(
1559
+ <ServiceDiscovery
1560
+ command={command}
1561
+ configureCommand={() => new Promise<void>(() => undefined)}
1562
+ onFieldChange={onFieldChange}
1563
+ />,
1564
+ );
1565
+
1566
+ command.serviceDiscoveryAssociations = [
1567
+ { containerName: null, containerPort: 8080, registry } as any,
1568
+ { containerName: 'worker', containerPort: 9090, registry } as any,
1569
+ ];
1570
+ command.useTaskDefinitionArtifact = true;
1571
+ wrapper.setProps({ command });
1572
+
1573
+ expect(wrapper.state('useTaskDefinitionArtifact')).toBe(true);
1574
+ expect(
1575
+ (wrapper.state('serviceDiscoveryAssociations') as any[]).map((association) => association.containerName),
1576
+ ).toEqual(['', 'worker']);
1577
+ expect(findByTestId(wrapper, 'ServiceDiscovery.containerName').map((input) => input.prop('value'))).toEqual([
1578
+ '',
1579
+ 'worker',
1580
+ ]);
1581
+ expect(onFieldChange).toHaveBeenCalledWith(
1582
+ 'serviceDiscoveryAssociations',
1583
+ wrapper.state('serviceDiscoveryAssociations'),
1584
+ );
1585
+ });
1586
+
1587
+ it('refreshes CapacityProvider options from rerendered command props without clobbering selections', () => {
1588
+ const strategy = [{ base: 0, capacityProvider: 'persisted-provider', weight: 1 }];
1589
+ const command = buildCommand({
1590
+ backingData: {
1591
+ filtered: {
1592
+ availableCapacityProviders: ['old-provider'],
1593
+ defaultCapacityProviderStrategy: [],
1594
+ },
1595
+ } as any,
1596
+ capacityProviderStrategy: strategy,
1597
+ ecsClusterName: 'old-cluster',
1598
+ useDefaultCapacityProviders: false,
1599
+ });
1600
+ const wrapper = shallow(
1601
+ <EcsCapacityProvider
1602
+ command={command}
1603
+ configureCommand={() => new Promise<void>(() => undefined)}
1604
+ onFieldChange={jasmine.createSpy('onFieldChange')}
1605
+ />,
1606
+ );
1607
+
1608
+ command.backingData.filtered.availableCapacityProviders = ['new-provider'];
1609
+ command.ecsClusterName = 'new-cluster';
1610
+ wrapper.setProps({ command });
1611
+
1612
+ expect(wrapper.state('capacityProviderStrategy')).toBe(strategy);
1613
+ expect(wrapper.state('availableCapacityProviders')).toEqual(['new-provider', 'persisted-provider']);
1614
+ expect(wrapper.state('ecsClusterName')).toBe('new-cluster');
1615
+ });
1616
+
1617
+ it('publishes the refreshed cluster strategy from the setState callback', async () => {
1618
+ const refreshedStrategy = [{ base: 1, capacityProvider: 'FARGATE_SPOT', weight: 2 }];
1619
+ const command = buildCommand({
1620
+ backingData: {
1621
+ filtered: {
1622
+ availableCapacityProviders: [],
1623
+ defaultCapacityProviderStrategy: [],
1624
+ },
1625
+ } as any,
1626
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
1627
+ useDefaultCapacityProviders: true,
1628
+ });
1629
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1630
+ const configureCommand = jasmine.createSpy('configureCommand').and.callFake(() => {
1631
+ command.backingData.filtered.defaultCapacityProviderStrategy = refreshedStrategy;
1632
+ return Promise.resolve();
1633
+ });
1634
+ const wrapper = shallow(
1635
+ <EcsCapacityProvider command={command} configureCommand={configureCommand} onFieldChange={onFieldChange} />,
1636
+ { disableLifecycleMethods: true } as any,
1637
+ );
1638
+ const instance = wrapper.instance() as any;
1639
+ let applyState: () => void;
1640
+ spyOn(instance, 'setState').and.callFake((state: any, callback?: () => void) => {
1641
+ applyState = () => {
1642
+ instance.state = { ...instance.state, ...state };
1643
+ callback?.();
1644
+ };
1645
+ });
1646
+
1647
+ instance.componentDidMount();
1648
+ await Promise.resolve();
1649
+ expect(onFieldChange).not.toHaveBeenCalledWith('capacityProviderStrategy', jasmine.anything());
1650
+
1651
+ applyState();
1652
+ expect(onFieldChange).toHaveBeenCalledWith('capacityProviderStrategy', refreshedStrategy);
1653
+ });
1654
+
1655
+ it('publishes refreshed default strategy props once without a notification loop', () => {
1656
+ const originalStrategy = [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }];
1657
+ const refreshedStrategy = [{ base: 1, capacityProvider: 'FARGATE_SPOT', weight: 2 }];
1658
+ const command = buildCommand({
1659
+ backingData: {
1660
+ filtered: {
1661
+ availableCapacityProviders: ['FARGATE', 'FARGATE_SPOT'],
1662
+ defaultCapacityProviderStrategy: originalStrategy,
1663
+ },
1664
+ } as any,
1665
+ capacityProviderStrategy: originalStrategy,
1666
+ ecsClusterName: 'cluster-a',
1667
+ useDefaultCapacityProviders: true,
1668
+ });
1669
+ const onFieldChange = jasmine.createSpy('onFieldChange').and.callFake((field: string, value: any) => {
1670
+ command[field] = value;
1671
+ });
1672
+ const wrapper = shallow(
1673
+ <EcsCapacityProvider
1674
+ command={command}
1675
+ configureCommand={() => new Promise<void>(() => undefined)}
1676
+ onFieldChange={onFieldChange}
1677
+ />,
1678
+ { disableLifecycleMethods: true } as any,
1679
+ );
1680
+
1681
+ command.backingData.filtered.defaultCapacityProviderStrategy = refreshedStrategy;
1682
+ (wrapper.instance() as EcsCapacityProvider).componentDidUpdate();
1683
+
1684
+ expect(wrapper.state('capacityProviderStrategy')).toBe(refreshedStrategy);
1685
+ expect(command.capacityProviderStrategy).toBe(refreshedStrategy);
1686
+ expect(onFieldChange).toHaveBeenCalledOnceWith('capacityProviderStrategy', refreshedStrategy);
1687
+
1688
+ (wrapper.instance() as EcsCapacityProvider).componentDidUpdate();
1689
+ expect(onFieldChange).toHaveBeenCalledTimes(1);
1690
+ });
1691
+
1692
+ it('gives interactive fields accessible names across all wizard pages', () => {
1693
+ const command = buildCommand({
1694
+ backingData: {
1695
+ accounts: ['test-account'],
1696
+ filtered: {
1697
+ availableCapacityProviders: ['FARGATE'],
1698
+ defaultCapacityProviderStrategy: [],
1699
+ ecsClusters: ['test-cluster'],
1700
+ iamRoles: ['ecs-role'],
1701
+ images: [{ imageId: 'registry/image:latest' }],
1702
+ regions: [{ name: 'us-east-1' }],
1703
+ secrets: ['docker-secret'],
1704
+ securityGroupNames: ['test-sg'],
1705
+ subnetTypes: [{ purpose: 'internal', vpcId: 'vpc-1' }],
1706
+ targetGroups: ['test-target'],
1707
+ },
1708
+ launchTypes: ['FARGATE'],
1709
+ networkModes: ['awsvpc'],
1710
+ } as any,
1711
+ capacity: { desired: 1, max: 2, min: 1 },
1712
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
1713
+ computeOption: 'launchType',
1714
+ containerMappings: [{ containerName: 'api', imageDescription: { imageId: 'registry/image:latest' } as any }],
1715
+ credentials: 'test-account',
1716
+ dockerImageCredentialsSecret: 'docker-secret',
1717
+ ecsClusterName: 'test-cluster',
1718
+ iamRole: 'ecs-role',
1719
+ imageDescription: { imageId: 'registry/image:latest' } as any,
1720
+ launchType: 'FARGATE',
1721
+ logDriver: 'awslogs',
1722
+ networkMode: 'awsvpc',
1723
+ placementConstraints: [{ expression: 'attribute:ecs.instance-type =~ t3.*', type: 'memberOf' }],
1724
+ region: 'us-east-1',
1725
+ serviceDiscoveryAssociations: [
1726
+ { containerName: 'api', containerPort: 8080, registry: { displayName: 'registry (registry-1)' } },
1727
+ ],
1728
+ targetGroupMappings: [{ containerName: 'api', containerPort: 8080, targetGroup: 'test-target' }],
1729
+ useDefaultCapacityProviders: false,
1730
+ useTaskDefinitionArtifact: true,
1731
+ });
1732
+ const pageProps = buildProps(command);
1733
+ const fieldChange = jasmine.createSpy('onFieldChange');
1734
+ const configureCommand = () => Promise.resolve();
1735
+ const props = { ...pageProps, configureCommand, onFieldChange: fieldChange };
1736
+
1737
+ const basic = shallow(<BasicSettings {...props} />);
1738
+ expect(basic.find(AccountSelectInput).prop('aria-label')).toBe('Account');
1739
+ expect(basic.find(RegionSelectInput).prop('aria-label')).toBe('Region');
1740
+ expect(findByTestId(basic, 'ServerGroup.clusterName').prop('aria-label')).toBe('ECS Cluster name');
1741
+ expect(findByTestId(basic, 'ServerGroup.stack').prop('aria-label')).toBe('Stack');
1742
+ expect(findByTestId(basic, 'ServerGroup.details').prop('aria-label')).toBe('Detail');
1743
+
1744
+ const networking = shallow(
1745
+ <EcsNetworking command={command} configureCommand={configureCommand} onFieldChange={fieldChange} />,
1746
+ { disableLifecycleMethods: true } as any,
1747
+ );
1748
+ expect(findByTestId(networking, 'Networking.networkMode').find(TetheredSelect).prop('inputProps')).toEqual({
1749
+ 'aria-label': 'Network mode',
1750
+ });
1751
+ expect(findByTestId(networking, 'Networking.subnetType').find(TetheredSelect).prop('inputProps')).toEqual({
1752
+ 'aria-label': 'VPC subnet',
1753
+ });
1754
+ expect(findByTestId(networking, 'Networking.securityGroups').find(TetheredSelect).prop('inputProps')).toEqual({
1755
+ 'aria-label': 'Security groups',
1756
+ });
1757
+
1758
+ const taskDefinition = shallow(
1759
+ <TaskDefinition command={command} configureCommand={configureCommand} onFieldChange={fieldChange} />,
1760
+ { disableLifecycleMethods: true } as any,
1761
+ );
1762
+ expect(findByTestId(taskDefinition, 'Artifacts.containerName').prop('aria-label')).toBe('Container name 1');
1763
+ expect(findByTestId(taskDefinition, 'Artifacts.containerImage').find(TetheredSelect).prop('inputProps')).toEqual({
1764
+ 'aria-label': 'Container image 1',
1765
+ });
1766
+ expect(findByTestId(taskDefinition, 'Artifacts.targetGroupContainer').prop('aria-label')).toBe(
1767
+ 'Target group container name 1',
1768
+ );
1769
+ expect(findByTestId(taskDefinition, 'Artifacts.targetGroup').find(TetheredSelect).prop('inputProps')).toEqual({
1770
+ 'aria-label': 'Target group 1',
1771
+ });
1772
+ expect(findByTestId(taskDefinition, 'Artifacts.targetGroupPort').prop('aria-label')).toBe('Target port 1');
1773
+
1774
+ const container = shallow(
1775
+ <Container command={command} configureCommand={configureCommand} onFieldChange={fieldChange} />,
1776
+ { disableLifecycleMethods: true } as any,
1777
+ );
1778
+ expect(findByTestId(container, 'ContainerInputs.containerImage').find(TetheredSelect).prop('inputProps')).toEqual({
1779
+ 'aria-label': 'Container image',
1780
+ });
1781
+ expect(findByTestId(container, 'ContainerInputs.computeUnits').prop('aria-label')).toBe('Compute units');
1782
+ expect(findByTestId(container, 'ContainerInputs.reservedMemory').prop('aria-label')).toBe('Reserved memory');
1783
+ expect(findByTestId(container, 'ContainerInputs.targetGroup').find(TetheredSelect).prop('inputProps')).toEqual({
1784
+ 'aria-label': 'Target group 1',
1785
+ });
1786
+ expect(findByTestId(container, 'ContainerInputs.targetGroupPort').prop('aria-label')).toBe('Target port 1');
1787
+
1788
+ const scaling = shallow(<HorizontalScalingSettings {...props} />);
1789
+ expect(findByTestId(scaling, 'ServerGroup.launchType').prop('aria-label')).toBe('Launch type');
1790
+ expect(findByTestId(scaling, 'ServerGroup.capacity.desired').prop('aria-label')).toBe('Desired capacity');
1791
+ expect(findByTestId(scaling, 'ServerGroup.capacity.min').prop('aria-label')).toBe('Minimum capacity');
1792
+ expect(findByTestId(scaling, 'ServerGroup.capacity.max').prop('aria-label')).toBe('Maximum capacity');
1793
+ const capacityProvider = renderCapacityProvider(command);
1794
+ expect(findByTestId(capacityProvider, 'ServerGroup.customCapacityProvider.name.0').prop('aria-label')).toBe(
1795
+ 'Capacity provider name 1',
1796
+ );
1797
+ expect(findByTestId(capacityProvider, 'ServerGroup.capacityProvider.base.0').prop('aria-label')).toBe(
1798
+ 'Capacity provider base 1',
1799
+ );
1800
+ expect(findByTestId(capacityProvider, 'ServerGroup.capacityProvider.weight.0').prop('aria-label')).toBe(
1801
+ 'Capacity provider weight 1',
1802
+ );
1803
+
1804
+ const logging = shallow(<LoggingSettings {...props} />);
1805
+ expect(findByTestId(logging, 'Logging.logDriver').prop('aria-label')).toBe('Log driver');
1806
+ expect(logging.find(MapEditor).prop('keyLabel')).toBe('Logging option');
1807
+ expect(logging.find(MapEditor).prop('valueLabel')).toBe('Logging option value');
1808
+
1809
+ const discovery = shallow(
1810
+ <ServiceDiscovery command={command} configureCommand={configureCommand} onFieldChange={fieldChange} />,
1811
+ { disableLifecycleMethods: true } as any,
1812
+ );
1813
+ expect(findByTestId(discovery, 'ServiceDiscovery.containerName').prop('aria-label')).toBe('Container name 1');
1814
+ expect(findByTestId(discovery, 'ServiceDiscovery.registry').find(TetheredSelect).prop('inputProps')).toEqual({
1815
+ 'aria-label': 'Service registry 1',
1816
+ });
1817
+ expect(findByTestId(discovery, 'ServiceDiscovery.containerPort').prop('aria-label')).toBe('Container port 1');
1818
+
1819
+ const advanced = shallow(
1820
+ <AdvancedSettings {...props} command={{ ...command, useTaskDefinitionArtifact: false }} />,
1821
+ );
1822
+ [
1823
+ ['Advanced.healthCheckGracePeriodSeconds', 'Health check grace period'],
1824
+ ['Advanced.iamRole', 'ECS IAM instance profile'],
1825
+ ['Advanced.dockerImageCredentialsSecret', 'Docker image credentials'],
1826
+ ['Advanced.platformVersion', 'Fargate platform version'],
1827
+ ['Advanced.enableDeploymentCircuitBreaker', 'Enable deployment circuit breaker'],
1828
+ ['Advanced.placementStrategyName', 'Placement strategy'],
1829
+ ['Advanced.placementConstraint.type.0', 'Placement constraint type 1'],
1830
+ ['Advanced.placementConstraint.expression.0', 'Placement constraint expression 1'],
1831
+ ].forEach(([testId, label]) => expect(findByTestId(advanced, testId).prop('aria-label')).toBe(label));
1832
+ expect(findByTestId(advanced, 'Advanced.dockerLabels').find(MapEditor).prop('keyLabel')).toBe('Docker label name');
1833
+ expect(findByTestId(advanced, 'Advanced.environmentVariables').find(MapEditor).prop('keyLabel')).toBe(
1834
+ 'Environment variable name',
1835
+ );
1836
+ expect(findByTestId(advanced, 'Advanced.tags').find(MapEditor).prop('keyLabel')).toBe('Tag name');
1837
+ });
1838
+
1839
+ it('restores Advanced Settings service, task, placement, and metadata controls', () => {
1840
+ const command = buildCommand({
1841
+ backingData: {
1842
+ filtered: {
1843
+ iamRoles: ['available-role'],
1844
+ secrets: ['available-secret'],
1845
+ },
1846
+ } as any,
1847
+ dockerImageCredentialsSecret: 'available-secret',
1848
+ dockerLabels: { component: 'api' },
1849
+ enableDeploymentCircuitBreaker: true,
1850
+ environmentVariables: { ENVIRONMENT: 'production' },
1851
+ healthCheckGracePeriodSeconds: 60,
1852
+ iamRole: 'available-role',
1853
+ placementConstraints: [{ expression: 'attribute:ecs.instance-type =~ t3.*', type: 'memberOf' }],
1854
+ placementStrategyName: 'BinPack CPU',
1855
+ platformVersion: '1.4.0',
1856
+ tags: { owner: 'payments' },
1857
+ useTaskDefinitionArtifact: false,
1858
+ });
1859
+ const onFieldChange = jasmine.createSpy('onFieldChange');
1860
+ const page = shallow(
1861
+ React.createElement(AdvancedSettings as any, {
1862
+ application: buildProps(command).application,
1863
+ command,
1864
+ onFieldChange,
1865
+ }),
1866
+ );
1867
+
1868
+ expect(findByTestId(page, 'Advanced.healthCheckGracePeriodSeconds').prop('value')).toBe(60);
1869
+ expect(findByTestId(page, 'Advanced.iamRole').prop('value')).toBe('available-role');
1870
+ expect(findByTestId(page, 'Advanced.dockerImageCredentialsSecret').prop('value')).toBe('available-secret');
1871
+ expect(findByTestId(page, 'Advanced.platformVersion').prop('value')).toBe('1.4.0');
1872
+ expect(findByTestId(page, 'Advanced.enableDeploymentCircuitBreaker').prop('checked')).toBe(true);
1873
+ expect(findByTestId(page, 'Advanced.placementStrategyName').prop('value')).toBe('BinPack CPU');
1874
+ expect(findByTestId(page, 'Advanced.placementConstraint.type.0').prop('value')).toBe('memberOf');
1875
+ expect(findByTestId(page, 'Advanced.placementConstraint.expression.0').prop('value')).toContain('t3.*');
1876
+ expect(findByTestId(page, 'Advanced.dockerLabels').find(MapEditor).prop('model')).toEqual({ component: 'api' });
1877
+ expect(findByTestId(page, 'Advanced.environmentVariables').find(MapEditor).prop('model')).toEqual({
1878
+ ENVIRONMENT: 'production',
1879
+ });
1880
+ expect(findByTestId(page, 'Advanced.tags').find(MapEditor).prop('model')).toEqual({ owner: 'payments' });
1881
+
1882
+ findByTestId(page, 'Advanced.placementStrategyName').simulate('change', { target: { value: 'One Task Per Host' } });
1883
+ expect(onFieldChange).toHaveBeenCalledWith('placementStrategyName', 'One Task Per Host');
1884
+
1885
+ const artifactPage = shallow(
1886
+ React.createElement(AdvancedSettings as any, {
1887
+ application: buildProps(command).application,
1888
+ command: { ...command, useTaskDefinitionArtifact: true },
1889
+ onFieldChange,
1890
+ }),
1891
+ );
1892
+ expect(findByTestId(artifactPage, 'Advanced.dockerImageCredentialsSecret').exists()).toBe(false);
1893
+ expect(findByTestId(artifactPage, 'Advanced.dockerLabels').exists()).toBe(false);
1894
+ expect(findByTestId(artifactPage, 'Advanced.environmentVariables').exists()).toBe(false);
1895
+ expect(findByTestId(artifactPage, 'Advanced.tags').exists()).toBe(true);
1896
+ });
1897
+
1898
+ it('only includes target groups from the selected account', () => {
1899
+ const command = buildCommand({ credentials: 'selected-account', region: 'us-east-1' });
1900
+ const modal = buildUnrenderedModal(command) as any;
1901
+ const loadBalancers = [
1902
+ {
1903
+ accounts: [
1904
+ {
1905
+ name: 'selected-account',
1906
+ regions: [
1907
+ {
1908
+ loadBalancers: [
1909
+ {
1910
+ targetGroups: ['selected-target', { targetGroupName: 'selected-object-target' }],
1911
+ },
1912
+ ],
1913
+ name: 'us-east-1',
1914
+ },
1915
+ ],
1916
+ },
1917
+ {
1918
+ name: 'other-account',
1919
+ regions: [
1920
+ {
1921
+ loadBalancers: [{ targetGroups: ['other-account-target'] }],
1922
+ name: 'us-east-1',
1923
+ },
1924
+ ],
1925
+ },
1926
+ ],
1927
+ },
1928
+ ];
1929
+
1930
+ expect(modal.getTargetGroups(command, loadBalancers)).toEqual(['selected-target', 'selected-object-target']);
1931
+ });
1932
+
1933
+ it('only includes target groups from the selected region while preserving deduplicated selections', () => {
1934
+ const command = buildCommand({
1935
+ credentials: 'selected-account',
1936
+ region: 'us-east-1',
1937
+ targetGroup: 'legacy-unavailable-target',
1938
+ targetGroupMappings: [
1939
+ { targetGroup: 'persisted-unavailable-target' } as any,
1940
+ { targetGroup: 'duplicate-target' } as any,
1941
+ ],
1942
+ });
1943
+ const modal = buildUnrenderedModal(command) as any;
1944
+ const loadBalancers = [
1945
+ {
1946
+ accounts: [
1947
+ {
1948
+ name: 'selected-account',
1949
+ regions: [
1950
+ {
1951
+ loadBalancers: [
1952
+ {
1953
+ targetGroups: ['duplicate-target', 'available-target', 'available-target'],
1954
+ },
1955
+ ],
1956
+ name: 'us-east-1',
1957
+ },
1958
+ {
1959
+ loadBalancers: [{ targetGroups: ['other-region-target'] }],
1960
+ name: 'eu-west-1',
1961
+ },
1962
+ ],
1963
+ },
1964
+ ],
1965
+ },
1966
+ ];
1967
+
1968
+ expect(modal.getTargetGroups(command, loadBalancers)).toEqual([
1969
+ 'duplicate-target',
1970
+ 'available-target',
1971
+ 'persisted-unavailable-target',
1972
+ 'legacy-unavailable-target',
1973
+ ]);
1974
+ });
1975
+
1976
+ it('starts custom capacity provider mode empty when switching from the cluster default', () => {
1977
+ const command = buildCommand({
1978
+ backingData: {
1979
+ filtered: {
1980
+ availableCapacityProviders: ['FARGATE_SPOT'],
1981
+ defaultCapacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE_SPOT', weight: 1 }],
1982
+ },
1983
+ } as any,
1984
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE_SPOT', weight: 1 }],
1985
+ credentials: 'ecs-account',
1986
+ ecsClusterName: 'cluster',
1987
+ region: 'eu-west-1',
1988
+ useDefaultCapacityProviders: true,
1989
+ });
1990
+ const wrapper = renderCapacityProvider(command);
1991
+
1992
+ findByTestId(wrapper, 'ServerGroup.capacityProviders.custom').simulate('click');
1993
+ findByTestId(wrapper, 'ServerGroup.addCapacityProvider').simulate('click');
1994
+
1995
+ expect(command.useDefaultCapacityProviders).toBe(false);
1996
+ expect(command.capacityProviderStrategy).toEqual([{ base: null, capacityProvider: '', weight: null } as any]);
1997
+ });
1998
+
1999
+ it('renders and updates each custom capacity provider row by index', () => {
2000
+ const command = buildCommand({
2001
+ capacityProviderMode: 'custom',
2002
+ capacityProviderStrategy: [
2003
+ { base: 0, capacityProvider: 'FARGATE', weight: 1 },
2004
+ { base: 1, capacityProvider: 'FARGATE_SPOT', weight: 2 },
2005
+ ],
2006
+ useDefaultCapacityProviders: false,
2007
+ });
2008
+ const wrapper = renderCapacityProvider(command);
2009
+ expect(findByTestId(wrapper, 'ServerGroup.capacityProvider.base.1').prop('value')).toBe(1);
2010
+
2011
+ findByTestId(wrapper, 'ServerGroup.capacityProvider.weight.1').simulate('change', {
2012
+ target: { valueAsNumber: 3 },
2013
+ });
2014
+
2015
+ expect(command.capacityProviderStrategy[1].weight).toBe(3);
2016
+ });
2017
+
2018
+ it('renders custom capacity provider names as standard inputs', () => {
2019
+ const command = buildCommand({
2020
+ capacityProviderMode: 'custom',
2021
+ capacityProviderStrategy: [{ base: 0, capacityProvider: 'FARGATE', weight: 1 }],
2022
+ useDefaultCapacityProviders: false,
2023
+ });
2024
+ const wrapper = renderCapacityProvider(command);
2025
+ const nameInput = findByTestId(wrapper, 'ServerGroup.customCapacityProvider.name.0');
2026
+
2027
+ expect(nameInput.type()).toBe('input');
2028
+ expect(nameInput.prop('value')).toBe('FARGATE');
2029
+
2030
+ nameInput.simulate('change', { target: { value: 'FARGATE_SPOT' } });
2031
+
2032
+ expect(command.capacityProviderStrategy[0].capacityProvider).toBe('FARGATE_SPOT');
2033
+ });
2034
+
2035
+ it('does not show custom capacity provider controls for matching default strategy values', () => {
2036
+ const command = buildCommand({
2037
+ capacityProviderMode: 'default',
2038
+ capacityProviderStrategy: [{ base: 1, capacityProvider: 'FARGATE_SPOT', weight: 2 }],
2039
+ useDefaultCapacityProviders: true,
2040
+ });
2041
+ const wrapper = renderCapacityProvider(command);
2042
+
2043
+ expect(findByTestId(wrapper, 'ServerGroup.addCapacityProvider').exists()).toBe(false);
2044
+ expect(findByTestId(wrapper, 'ServerGroup.defaultCapacityProvider.name.0').prop('value')).toBe('FARGATE_SPOT');
2045
+ });
2046
+
2047
+ it('does not synthesize FARGATE_SPOT when the cluster has no default capacity provider strategy', () => {
2048
+ const command = buildCommand({
2049
+ backingData: { ...buildCommand().backingData, filtered: { defaultCapacityProviderStrategy: [] } } as any,
2050
+ capacityProviderStrategy: [],
2051
+ useDefaultCapacityProviders: false,
2052
+ });
2053
+ const wrapper = renderCapacityProvider(command);
2054
+
2055
+ findByTestId(wrapper, 'ServerGroup.capacityProviders.default').simulate('click');
2056
+
2057
+ expect(command.capacityProviderStrategy).toEqual([]);
2058
+ expect(wrapper.state('capacityProviderStrategy')).toEqual([]);
2059
+ });
2060
+
2061
+ it('renders a capacity provider option only for the active custom row', () => {
2062
+ const command = buildCommand({
2063
+ capacityProviderMode: 'custom',
2064
+ capacityProviderStrategy: [
2065
+ { base: 0, capacityProvider: 'FARGATE', weight: 1 },
2066
+ { base: 1, capacityProvider: 'FARGATE_SPOT', weight: 2 },
2067
+ ],
2068
+ useDefaultCapacityProviders: false,
2069
+ });
2070
+ const wrapper = renderCapacityProvider(command);
2071
+ wrapper.setState({ activeCapacityProviderIndex: null });
2072
+ expect(wrapper.find('.Select-option').length).toBe(0);
2073
+
2074
+ wrapper.setState({ activeCapacityProviderIndex: 1 });
2075
+ expect(wrapper.find('.Select-option').length).toBeGreaterThan(0);
2076
+ expect(findByTestId(wrapper, 'ServerGroup.customCapacityProvider.name.1').exists()).toBe(true);
2077
+ });
2078
+ });