@spinnaker/google 2026.3.2 → 2026.4.1

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 (108) hide show
  1. package/dist/autoscalingPolicy/GceAutoScalingFieldLayout.d.ts +2 -2
  2. package/dist/domain/loadBalancer.d.ts +6 -3
  3. package/dist/image/ImageSelect.d.ts +1 -1
  4. package/dist/index.js +159 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/loadBalancer/configure/common/gceLoadBalancerModels.d.ts +13 -2
  7. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerEditor.d.ts +3 -0
  8. package/dist/loadBalancer/configure/http/GceHttpLoadBalancerModal.d.ts +1 -1
  9. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.d.ts +30 -0
  10. package/dist/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.d.ts +53 -0
  11. package/dist/loadBalancer/httpLoadBalancerUtils.service.d.ts +3 -1
  12. package/dist/serverGroup/configure/wizard/GceCloneServerGroupModal.d.ts +3 -0
  13. package/dist/serverGroup/configure/wizard/GceServerGroupWizard.types.d.ts +1 -0
  14. package/dist/serverGroup/configure/wizard/customInstance/CustomInstanceConfigurer.d.ts +1 -1
  15. package/dist/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.d.ts +2 -2
  16. package/dist/serverGroup/configure/wizard/pages/AdvancedSettings.d.ts +3 -0
  17. package/dist/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.d.ts +1 -0
  18. package/dist/serverGroup/configure/wizard/pages/Policies.d.ts +21 -3
  19. package/dist/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.d.ts +1 -0
  20. package/dist/serverGroup/configure/wizard/pages/ServerGroupCapacity.d.ts +1 -0
  21. package/dist/serverGroup/configure/wizard/pages/ServerGroupImageSettings.d.ts +1 -0
  22. package/dist/serverGroup/configure/wizard/pages/ServerGroupInstanceType.d.ts +1 -0
  23. package/dist/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.d.ts +2 -2
  24. package/dist/serverGroup/details/GceInstanceFlexibilityPolicyDetails.d.ts +2 -2
  25. package/dist/serverGroup/details/stateful/MarkDiskStatefulButton.d.ts +2 -2
  26. package/dist/serverGroup/details/stateful/UpdateBootImageButton.d.ts +2 -2
  27. package/package.json +5 -5
  28. package/src/autoHealingPolicy/GceAutoHealingPolicyEditor.spec.tsx +68 -52
  29. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.spec.tsx +43 -50
  30. package/src/autoscalingPolicy/GceAutoscalingPolicyEditor.tsx +1 -1
  31. package/src/autoscalingPolicy/GceAutoscalingPolicyWriter.spec.ts +1 -1
  32. package/src/domain/loadBalancer.ts +6 -3
  33. package/src/gce.module.spec.ts +132 -153
  34. package/src/instance/details/GceInstanceDetails.spec.tsx +86 -83
  35. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.spec.tsx +162 -121
  36. package/src/loadBalancer/configure/choice/GceLoadBalancerChoiceModal.tsx +13 -1
  37. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.spec.tsx +47 -30
  38. package/src/loadBalancer/configure/common/GceProxyLoadBalancerEditor.tsx +2 -1
  39. package/src/loadBalancer/configure/common/GceProxyLoadBalancerModal.spec.tsx +85 -57
  40. package/src/loadBalancer/configure/common/gceLoadBalancerData.spec.ts +17 -16
  41. package/src/loadBalancer/configure/common/gceLoadBalancerModels.spec.ts +230 -5
  42. package/src/loadBalancer/configure/common/gceLoadBalancerModels.ts +186 -22
  43. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.spec.ts +298 -2
  44. package/src/loadBalancer/configure/common/gceLoadBalancerSubmission.ts +35 -5
  45. package/src/loadBalancer/configure/common/useGceLoadBalancerData.spec.tsx +37 -46
  46. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.spec.tsx +399 -58
  47. package/src/loadBalancer/configure/http/GceHttpLoadBalancerEditor.tsx +148 -41
  48. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.spec.tsx +169 -36
  49. package/src/loadBalancer/configure/http/GceHttpLoadBalancerListenerEditor.tsx +80 -12
  50. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.spec.tsx +224 -124
  51. package/src/loadBalancer/configure/http/GceHttpLoadBalancerModal.tsx +8 -2
  52. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.spec.tsx +57 -22
  53. package/src/loadBalancer/configure/http/GceHttpLoadBalancerResourceEditors.tsx +21 -0
  54. package/src/loadBalancer/configure/http/GceHttpLoadBalancerRoutingEditor.spec.tsx +24 -20
  55. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerEditor.spec.tsx +41 -32
  56. package/src/loadBalancer/configure/network/GceNetworkLoadBalancerModal.spec.tsx +35 -30
  57. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.spec.tsx +443 -0
  58. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerEditor.tsx +401 -0
  59. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.spec.tsx +330 -0
  60. package/src/loadBalancer/configure/network/GceRegionalExternalNetworkLoadBalancerModal.tsx +348 -0
  61. package/src/loadBalancer/details/gceLoadBalancerDetails.spec.tsx +439 -35
  62. package/src/loadBalancer/details/gceLoadBalancerDetails.tsx +104 -21
  63. package/src/loadBalancer/httpLoadBalancerUtils.service.spec.ts +126 -0
  64. package/src/loadBalancer/httpLoadBalancerUtils.service.ts +27 -11
  65. package/src/loadBalancer/loadBalancer.setTransformer.spec.ts +165 -0
  66. package/src/loadBalancer/loadBalancer.setTransformer.ts +15 -2
  67. package/src/pipeline/stages/googleStageConfig.spec.js +22 -16
  68. package/src/securityGroup/configure/GceSecurityGroupModal.spec.tsx +177 -193
  69. package/src/securityGroup/configure/GceSecurityGroupModal.tsx +14 -2
  70. package/src/securityGroup/details/GceSecurityGroupDetails.spec.tsx +69 -78
  71. package/src/serverGroup/configure/serverGroupCommandBuilder.service.spec.js +35 -15
  72. package/src/serverGroup/configure/serverGroupConfiguration.service.js +7 -1
  73. package/src/serverGroup/configure/serverGroupConfiguration.service.spec.js +74 -4
  74. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.spec.tsx +593 -855
  75. package/src/serverGroup/configure/wizard/GceCloneServerGroupModal.tsx +120 -38
  76. package/src/serverGroup/configure/wizard/GceServerGroupWizard.types.ts +1 -0
  77. package/src/serverGroup/configure/wizard/GceServerGroupWizardPage.spec.tsx +53 -50
  78. package/src/serverGroup/configure/wizard/customInstance/GceCustomInstanceBuilder.spec.tsx +65 -49
  79. package/src/serverGroup/configure/wizard/hiddenMetadataKeys.value.js +1 -0
  80. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.spec.tsx +101 -108
  81. package/src/serverGroup/configure/wizard/pages/AdvancedSettings.tsx +32 -28
  82. package/src/serverGroup/configure/wizard/pages/GceServerGroupFirewalls.spec.tsx +60 -59
  83. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.spec.tsx +240 -114
  84. package/src/serverGroup/configure/wizard/pages/GceServerGroupLoadBalancers.tsx +152 -63
  85. package/src/serverGroup/configure/wizard/pages/Policies.spec.tsx +93 -48
  86. package/src/serverGroup/configure/wizard/pages/Policies.tsx +80 -55
  87. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.spec.tsx +123 -94
  88. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +24 -18
  89. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.spec.tsx +152 -133
  90. package/src/serverGroup/configure/wizard/pages/ServerGroupCapacity.tsx +40 -37
  91. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.spec.tsx +66 -60
  92. package/src/serverGroup/configure/wizard/pages/ServerGroupImageSettings.tsx +8 -2
  93. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.spec.tsx +131 -101
  94. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +29 -28
  95. package/src/serverGroup/configure/wizard/zones/GceInstanceFlexibilityConfigurer.spec.tsx +104 -81
  96. package/src/serverGroup/details/GceInstanceFlexibilityPolicyDetails.spec.tsx +10 -11
  97. package/src/serverGroup/details/autoHealingPolicy/GceAutoHealingPolicyDetails.spec.tsx +51 -59
  98. package/src/serverGroup/details/autoHealingPolicy/modal/GceUpsertAutoHealingPolicyModal.spec.tsx +3 -3
  99. package/src/serverGroup/details/autoscalingPolicy/GceAutoscalingPolicyDetails.spec.tsx +49 -58
  100. package/src/serverGroup/details/autoscalingPolicy/modal/GceUpsertAutoscalingPolicyModal.spec.tsx +3 -3
  101. package/src/serverGroup/details/gceServerGroupDetails.spec.tsx +162 -141
  102. package/src/serverGroup/details/resize/GceResizeServerGroupModal.spec.tsx +102 -86
  103. package/src/serverGroup/details/resize/GceResizeServerGroupModal.tsx +4 -0
  104. package/src/serverGroup/details/rollback/GceRollbackServerGroupModal.spec.tsx +40 -31
  105. package/src/serverGroup/serverGroup.transformer.js +1 -0
  106. package/src/serverGroup/serverGroup.transformer.spec.js +48 -0
  107. package/src/validation/ApplicationNameValidator.spec.ts +1 -1
  108. /package/dist/{gce.logo2ae85d8704b1d6b8.png → gce.logoChI3w_A5.png} +0 -0
@@ -1,6 +1,8 @@
1
- import { mount as enzymeMount } from 'enzyme';
1
+ import type { Mock } from 'vitest';
2
2
  import type { FormikProps } from 'formik';
3
3
  import React from 'react';
4
+ import { fireEvent, render } from '@testing-library/react';
5
+ import type { RenderResult } from '@testing-library/react';
4
6
 
5
7
  import { DeckRuntimeContext } from '@spinnaker/core';
6
8
 
@@ -9,21 +11,16 @@ import type {
9
11
  IGceServerGroupWizardAdapter,
10
12
  IGceServerGroupWizardPageProps,
11
13
  } from '../GceServerGroupWizard.types';
12
- import { ServerGroupInstanceType } from './ServerGroupInstanceType';
14
+ import { ServerGroupInstanceType, validateGceServerGroupInstanceType } from './ServerGroupInstanceType';
13
15
 
14
16
  describe('ServerGroupInstanceType', () => {
15
17
  let runtimeServices: any;
16
- const RuntimeWrapper = ({ children }: React.PropsWithChildren<{}>) => (
17
- <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>{children}</DeckRuntimeContext.Provider>
18
- );
19
- const shallow = (component: React.ReactElement) => enzymeMount(component, { wrappingComponent: RuntimeWrapper });
18
+ const renderPage = (component: React.ReactElement) => renderWithRuntime(component, runtimeServices);
20
19
 
21
20
  beforeEach(() => {
22
21
  runtimeServices = {
23
22
  instanceTypeService: {
24
- getInstanceTypeDetails: jasmine
25
- .createSpy('getInstanceTypeDetails')
26
- .and.returnValue(new Promise(() => undefined)),
23
+ getInstanceTypeDetails: vi.fn().mockReturnValue(new Promise(() => undefined)),
27
24
  },
28
25
  };
29
26
  });
@@ -35,9 +32,9 @@ describe('ServerGroupInstanceType', () => {
35
32
  acceleratorConfigs: [{ acceleratorType: 'retired-gpu', acceleratorCount: 3 }],
36
33
  });
37
34
 
38
- const wrapper = shallow(<ServerGroupInstanceType {...props(values)} />);
35
+ const wrapper = renderPage(<ServerGroupInstanceType {...props(values)} />);
39
36
 
40
- expect(wrapper.find('label[htmlFor="gce-machine-type"]').text()).toContain('Machine type');
37
+ expect(wrapper.getByText(/Machine type/, { selector: 'label' })).toHaveAttribute('for', 'gce-machine-type');
41
38
  expect(optionText(wrapper, '#gce-machine-type')).toContain('retired-machine (Unavailable)');
42
39
  expect(optionText(wrapper, '#gce-min-cpu-platform')).toContain('Retired CPU (Unavailable)');
43
40
  expect(optionText(wrapper, '#gce-accelerator-type-0')).toContain('retired-gpu (Unavailable)');
@@ -46,19 +43,19 @@ describe('ServerGroupInstanceType', () => {
46
43
 
47
44
  it('selects standard machine types without mutating current Formik values', () => {
48
45
  runtimeServices.instanceTypeService = {
49
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails').and.returnValue(new Promise(() => undefined)),
46
+ getInstanceTypeDetails: vi.fn().mockReturnValue(new Promise(() => undefined)),
50
47
  };
51
48
  const values = command({ instanceType: 'n1-standard-1' });
52
49
  const testProps = props(values);
53
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
50
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
54
51
 
55
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n2-standard-2' } });
52
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n2-standard-2' } });
56
53
 
57
54
  expect(values.instanceType).toBe('n1-standard-1');
58
55
  expect(testProps.formik.setValues).toHaveBeenCalledWith(
59
- jasmine.objectContaining({
56
+ expect.objectContaining({
60
57
  instanceType: 'n2-standard-2',
61
- viewState: jasmine.objectContaining({ instanceProfile: 'custom' }),
58
+ viewState: expect.objectContaining({ instanceProfile: 'custom' }),
62
59
  }),
63
60
  );
64
61
  });
@@ -70,7 +67,7 @@ describe('ServerGroupInstanceType', () => {
70
67
  ].forEach(({ description, instanceType }) => {
71
68
  it(`publishes pipeline ${description} machine types without concrete type reconciliation`, () => {
72
69
  const instanceTypeService = {
73
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails'),
70
+ getInstanceTypeDetails: vi.fn(),
74
71
  };
75
72
  runtimeServices.instanceTypeService = instanceTypeService;
76
73
  const values = command({
@@ -78,11 +75,11 @@ describe('ServerGroupInstanceType', () => {
78
75
  });
79
76
  const adapter = adapterForHandlers();
80
77
  const testProps = props(values, adapter);
81
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
78
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
82
79
 
83
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: instanceType } });
80
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: instanceType } });
84
81
 
85
- expect(testProps.formik.setValues).toHaveBeenCalledWith(jasmine.objectContaining({ instanceType }));
82
+ expect(testProps.formik.setValues).toHaveBeenCalledWith(expect.objectContaining({ instanceType }));
86
83
  expect(instanceTypeService.getInstanceTypeDetails).not.toHaveBeenCalled();
87
84
  expect(adapter.applyCommandHandler).not.toHaveBeenCalled();
88
85
  });
@@ -91,7 +88,7 @@ describe('ServerGroupInstanceType', () => {
91
88
  it('loads standard machine type details and reconciles storage and accelerator capabilities', async () => {
92
89
  const detailsRequest = deferred<any>();
93
90
  const instanceTypeService = {
94
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails').and.returnValue(detailsRequest.promise),
91
+ getInstanceTypeDetails: vi.fn().mockReturnValue(detailsRequest.promise),
95
92
  };
96
93
  runtimeServices.instanceTypeService = instanceTypeService;
97
94
  const values = command({
@@ -108,7 +105,7 @@ describe('ServerGroupInstanceType', () => {
108
105
  },
109
106
  });
110
107
  const adapter = adapterForHandlers();
111
- (adapter.applyCommandHandler as jasmine.Spy).and.callFake(async (next, handler) => ({
108
+ (adapter.applyCommandHandler as Mock).mockImplementation(async (next, handler) => ({
112
109
  command: {
113
110
  ...next,
114
111
  acceleratorConfigs: [],
@@ -121,9 +118,9 @@ describe('ServerGroupInstanceType', () => {
121
118
  result: { dirty: {} },
122
119
  }));
123
120
  const testProps = props(values, adapter);
124
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
121
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
125
122
 
126
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n2-standard-2' } });
123
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n2-standard-2' } });
127
124
 
128
125
  expect(instanceTypeService.getInstanceTypeDetails).toHaveBeenCalledWith('gce', 'n2-standard-2');
129
126
  detailsRequest.resolve({
@@ -136,16 +133,16 @@ describe('ServerGroupInstanceType', () => {
136
133
  await settle();
137
134
 
138
135
  expect(adapter.applyCommandHandler).toHaveBeenCalledWith(
139
- jasmine.objectContaining({
136
+ expect.objectContaining({
140
137
  instanceType: 'n2-standard-2',
141
138
  disks: [{ type: 'pd-ssd', sizeGb: 20 }],
142
- viewState: jasmine.objectContaining({
143
- instanceTypeDetails: jasmine.objectContaining({ name: 'n2-standard-2' }),
139
+ viewState: expect.objectContaining({
140
+ instanceTypeDetails: expect.objectContaining({ name: 'n2-standard-2' }),
144
141
  }),
145
142
  }),
146
143
  'zoneChanged',
147
144
  );
148
- const published = (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0];
145
+ const published = (testProps.formik.setValues as Mock).mock.lastCall[0];
149
146
  expect(published.disks).toEqual([{ type: 'pd-ssd', sizeGb: 20 }]);
150
147
  expect(published.viewState.overriddenStorageDescription).toBeUndefined();
151
148
  expect(published.viewState.instanceTypeDetails.storage.localSSDSupported).toBe(false);
@@ -157,9 +154,9 @@ describe('ServerGroupInstanceType', () => {
157
154
  const oldRequest = deferred<any>();
158
155
  const newRequest = deferred<any>();
159
156
  const instanceTypeService = {
160
- getInstanceTypeDetails: jasmine
161
- .createSpy('getInstanceTypeDetails')
162
- .and.callFake((_provider, instanceType) =>
157
+ getInstanceTypeDetails: vi
158
+ .fn()
159
+ .mockImplementation((_provider, instanceType) =>
163
160
  instanceType === 'n2-standard-2' ? oldRequest.promise : newRequest.promise,
164
161
  ),
165
162
  };
@@ -167,18 +164,18 @@ describe('ServerGroupInstanceType', () => {
167
164
  const values = command();
168
165
  const adapter = adapterForHandlers();
169
166
  const testProps = props(values, adapter);
170
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
167
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
171
168
 
172
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n2-standard-2' } });
173
- setFormikValues(wrapper, testProps, (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0]);
174
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n1-standard-1' } });
169
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n2-standard-2' } });
170
+ setFormikValues(wrapper, testProps, (testProps.formik.setValues as Mock).mock.lastCall[0], runtimeServices);
171
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n1-standard-1' } });
175
172
 
176
173
  newRequest.resolve(instanceTypeDetails('n1-standard-1', 30));
177
174
  await settle();
178
175
  oldRequest.resolve(instanceTypeDetails('n2-standard-2', 40));
179
176
  await settle();
180
177
 
181
- const published = (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0];
178
+ const published = (testProps.formik.setValues as Mock).mock.lastCall[0];
182
179
  expect(published.instanceType).toBe('n1-standard-1');
183
180
  expect(published.viewState.instanceTypeDetails.name).toBe('n1-standard-1');
184
181
  expect(published.disks).toEqual([{ type: 'pd-ssd', sizeGb: 30 }]);
@@ -188,48 +185,51 @@ describe('ServerGroupInstanceType', () => {
188
185
  it('ignores standard machine type details after the user switches to a custom type', async () => {
189
186
  const detailsRequest = deferred<any>();
190
187
  runtimeServices.instanceTypeService = {
191
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails').and.returnValue(detailsRequest.promise),
188
+ getInstanceTypeDetails: vi.fn().mockReturnValue(detailsRequest.promise),
192
189
  };
193
190
  const values = command();
194
191
  const adapter = adapterForHandlers();
195
192
  const testProps = props(values, adapter);
196
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
193
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
197
194
 
198
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n2-standard-2' } });
199
- setFormikValues(wrapper, testProps, (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0]);
200
- wrapper.find('#gce-machine-type-custom').simulate('change');
195
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n2-standard-2' } });
196
+ setFormikValues(wrapper, testProps, (testProps.formik.setValues as Mock).mock.lastCall[0], runtimeServices);
197
+ fireEvent.click(control(wrapper, '#gce-machine-type-custom'));
201
198
  await settle();
202
199
  detailsRequest.resolve(instanceTypeDetails('n2-standard-2', 40));
203
200
  await settle();
204
201
 
205
- const published = (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0];
202
+ const published = (testProps.formik.setValues as Mock).mock.lastCall[0];
206
203
  expect(published.instanceType).toContain('custom-');
207
204
  expect(published.viewState.instanceTypeDetails).toBeUndefined();
208
205
  expect(published.disks).toEqual([{ type: 'pd-ssd', sizeGb: 20 }]);
209
- expect((adapter.applyCommandHandler as jasmine.Spy).calls.allArgs().map((args) => args[1])).toEqual([
210
- 'customInstanceChanged',
211
- ]);
206
+ expect((adapter.applyCommandHandler as Mock).mock.calls.map((args) => args[1])).toEqual(['customInstanceChanged']);
212
207
  });
213
208
 
214
209
  it('preserves a disk edit made while machine type details are loading', async () => {
215
210
  const detailsRequest = deferred<any>();
216
211
  runtimeServices.instanceTypeService = {
217
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails').and.returnValue(detailsRequest.promise),
212
+ getInstanceTypeDetails: vi.fn().mockReturnValue(detailsRequest.promise),
218
213
  };
219
214
  const values = command();
220
215
  const testProps = props(values);
221
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
222
-
223
- wrapper.find('#gce-machine-type').simulate('change', { target: { value: 'n2-standard-2' } });
224
- const selectedValues = (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0];
225
- setFormikValues(wrapper, testProps, {
226
- ...selectedValues,
227
- disks: [{ type: 'pd-standard', sizeGb: 500 }],
228
- });
216
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
217
+
218
+ fireEvent.change(control(wrapper, '#gce-machine-type'), { target: { value: 'n2-standard-2' } });
219
+ const selectedValues = (testProps.formik.setValues as Mock).mock.lastCall[0];
220
+ setFormikValues(
221
+ wrapper,
222
+ testProps,
223
+ {
224
+ ...selectedValues,
225
+ disks: [{ type: 'pd-standard', sizeGb: 500 }],
226
+ },
227
+ runtimeServices,
228
+ );
229
229
  detailsRequest.resolve(instanceTypeDetails('n2-standard-2', 40));
230
230
  await settle();
231
231
 
232
- const published = (testProps.formik.setValues as jasmine.Spy).calls.mostRecent().args[0];
232
+ const published = (testProps.formik.setValues as Mock).mock.lastCall[0];
233
233
  expect(published.instanceType).toBe('n2-standard-2');
234
234
  expect(published.viewState.instanceTypeDetails.name).toBe('n2-standard-2');
235
235
  expect(published.disks).toEqual([{ type: 'pd-standard', sizeGb: 500 }]);
@@ -247,22 +247,22 @@ describe('ServerGroupInstanceType', () => {
247
247
  });
248
248
  const adapter = adapterForHandlers();
249
249
  const testProps = props(values, adapter);
250
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
250
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
251
251
 
252
- wrapper.find('#gce-custom-cpu').simulate('change', { target: { value: '8' } });
252
+ fireEvent.change(control(wrapper, '#gce-custom-cpu'), { target: { value: '8' } });
253
253
  await settle();
254
254
 
255
255
  expect(adapter.applyCommandHandler).toHaveBeenCalledWith(
256
- jasmine.objectContaining({
256
+ expect.objectContaining({
257
257
  instanceType: 'n2-custom-8-16384',
258
- viewState: jasmine.objectContaining({
259
- customInstance: jasmine.objectContaining({ vCpuCount: 8, memory: 16 }),
258
+ viewState: expect.objectContaining({
259
+ customInstance: expect.objectContaining({ vCpuCount: 8, memory: 16 }),
260
260
  }),
261
261
  }),
262
262
  'customInstanceChanged',
263
263
  );
264
264
  expect(testProps.formik.setValues).toHaveBeenCalledWith(
265
- jasmine.objectContaining({ instanceType: 'n2-custom-8-16384', handledBy: 'customInstanceChanged' }),
265
+ expect.objectContaining({ instanceType: 'n2-custom-8-16384', handledBy: 'customInstanceChanged' }),
266
266
  );
267
267
  expect(values.instanceType).toBe('n2-custom-4-16384');
268
268
  });
@@ -270,12 +270,12 @@ describe('ServerGroupInstanceType', () => {
270
270
  it('keeps spot scheduling fields compatible when preemptibility changes', () => {
271
271
  const values = command({ preemptible: false, automaticRestart: true, onHostMaintenance: 'MIGRATE' });
272
272
  const testProps = props(values);
273
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
273
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
274
274
 
275
- wrapper.find('#gce-preemptible').simulate('change', { target: { checked: true } });
275
+ fireEvent.click(control(wrapper, '#gce-preemptible'));
276
276
 
277
277
  expect(testProps.formik.setValues).toHaveBeenCalledWith(
278
- jasmine.objectContaining({ preemptible: true, automaticRestart: false, onHostMaintenance: 'TERMINATE' }),
278
+ expect.objectContaining({ preemptible: true, automaticRestart: false, onHostMaintenance: 'TERMINATE' }),
279
279
  );
280
280
  expect(values.preemptible).toBe(false);
281
281
  });
@@ -288,12 +288,12 @@ describe('ServerGroupInstanceType', () => {
288
288
  ],
289
289
  });
290
290
  const testProps = props(values);
291
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
291
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
292
292
 
293
- wrapper.find('#gce-accelerator-type-0').simulate('change', { target: { value: 'nvidia-tesla-v100' } });
293
+ fireEvent.change(control(wrapper, '#gce-accelerator-type-0'), { target: { value: 'nvidia-tesla-v100' } });
294
294
 
295
295
  expect(testProps.formik.setValues).toHaveBeenCalledWith(
296
- jasmine.objectContaining({
296
+ expect.objectContaining({
297
297
  acceleratorConfigs: [
298
298
  { acceleratorType: 'nvidia-tesla-v100', acceleratorCount: 2 },
299
299
  { acceleratorType: 'retired-gpu', acceleratorCount: 3 },
@@ -312,14 +312,14 @@ describe('ServerGroupInstanceType', () => {
312
312
  ],
313
313
  });
314
314
  const testProps = props(values);
315
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
315
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
316
316
 
317
- expect(wrapper.find('#gce-boot-disk-size').prop('min')).toBe(10);
318
- expect(wrapper.find('#gce-boot-disk-size').prop('max')).toBe(65536);
319
- wrapper.find('#gce-boot-disk-type').simulate('change', { target: { value: 'pd-standard' } });
317
+ expect(control(wrapper, '#gce-boot-disk-size')).toHaveAttribute('min', '10');
318
+ expect(control(wrapper, '#gce-boot-disk-size')).toHaveAttribute('max', '65536');
319
+ fireEvent.change(control(wrapper, '#gce-boot-disk-type'), { target: { value: 'pd-standard' } });
320
320
 
321
321
  expect(testProps.formik.setValues).toHaveBeenCalledWith(
322
- jasmine.objectContaining({
322
+ expect.objectContaining({
323
323
  disks: [
324
324
  { type: 'pd-standard', sizeGb: 20 },
325
325
  { type: 'local-ssd', sizeGb: 375 },
@@ -331,14 +331,13 @@ describe('ServerGroupInstanceType', () => {
331
331
  });
332
332
 
333
333
  it('accepts pipeline expressions but validates finite concrete disk and accelerator values', () => {
334
- const page = shallow(<ServerGroupInstanceType {...props(command())} />).instance() as ServerGroupInstanceType;
335
334
  const invalid = command({
336
335
  instanceType: '',
337
336
  disks: [{ type: 'pd-ssd', sizeGb: Infinity }],
338
337
  acceleratorConfigs: [{ acceleratorType: 'nvidia-tesla-t4', acceleratorCount: 3 }],
339
338
  });
340
339
 
341
- expect(page.validate(invalid)).toEqual({
340
+ expect(validateGceServerGroupInstanceType(invalid)).toEqual({
342
341
  instanceType: 'Machine type required.',
343
342
  disks: 'Boot disk size must be between 10 and 65536 GB.',
344
343
  acceleratorConfigs: 'Accelerator count is unavailable for the selected type.',
@@ -349,10 +348,10 @@ describe('ServerGroupInstanceType', () => {
349
348
  disks: [{ type: 'pd-ssd', sizeGb: '${ parameters.diskSize }' }],
350
349
  acceleratorConfigs: [{ acceleratorType: 'retired-gpu', acceleratorCount: '${ parameters.gpuCount }' }],
351
350
  });
352
- expect(page.validate(expressionValues)).toEqual({});
353
- const expressionWrapper = shallow(<ServerGroupInstanceType {...props(expressionValues)} />);
354
- expect(expressionWrapper.find('#gce-boot-disk-size').prop('type')).toBe('text');
355
- expect(expressionWrapper.find('#gce-accelerator-count-0').prop('type')).toBe('text');
351
+ expect(validateGceServerGroupInstanceType(expressionValues)).toEqual({});
352
+ const expressionWrapper = renderPage(<ServerGroupInstanceType {...props(expressionValues)} />);
353
+ expect(control(expressionWrapper, '#gce-boot-disk-size')).toHaveAttribute('type', 'text');
354
+ expect(control(expressionWrapper, '#gce-accelerator-count-0')).toHaveAttribute('type', 'text');
356
355
  });
357
356
 
358
357
  it('renders and associates instance type, boot disk, and accelerator validation errors', () => {
@@ -367,7 +366,7 @@ describe('ServerGroupInstanceType', () => {
367
366
  acceleratorConfigs: 'Accelerator count is unavailable for the selected type.',
368
367
  };
369
368
  const testProps = props(values, adapterForHandlers(), errors);
370
- const wrapper = shallow(<ServerGroupInstanceType {...testProps} />);
369
+ const wrapper = renderPage(<ServerGroupInstanceType {...testProps} />);
371
370
 
372
371
  [
373
372
  ['#gce-machine-type', 'gce-machine-type-error'],
@@ -389,7 +388,11 @@ describe('ServerGroupInstanceType', () => {
389
388
  acceleratorTypes: [],
390
389
  },
391
390
  });
392
- wrapper.setProps({ formik: { ...testProps.formik, errors, values: customValues } });
391
+ rerenderInstanceType(
392
+ wrapper,
393
+ { ...testProps, formik: { ...testProps.formik, errors, values: customValues } },
394
+ runtimeServices,
395
+ );
393
396
 
394
397
  expectAssociatedError(wrapper, '#gce-custom-cpu', 'gce-machine-type-error');
395
398
  expectAssociatedError(wrapper, '#gce-custom-memory', 'gce-machine-type-error');
@@ -397,24 +400,52 @@ describe('ServerGroupInstanceType', () => {
397
400
  });
398
401
  });
399
402
 
400
- function expectAssociatedError(wrapper: any, selector: string, errorId: string): void {
401
- const control = wrapper.find(selector);
402
- expect(control.prop('aria-invalid')).withContext(selector).toBe(true);
403
- expect(control.prop('aria-describedby')).withContext(selector).toBe(errorId);
403
+ function expectAssociatedError(wrapper: RenderResult, selector: string, errorId: string): void {
404
+ const element = control(wrapper, selector);
405
+ expect(element, selector).toHaveAttribute('aria-invalid', 'true');
406
+ expect(element, selector).toHaveAttribute('aria-describedby', errorId);
407
+ }
408
+
409
+ function expectValidationAlert(wrapper: RenderResult, id: string, message: string): void {
410
+ const alert = control(wrapper, `#${id}`);
411
+ expect(alert, id).toHaveAttribute('role', 'alert');
412
+ expect(alert, id).toHaveTextContent(message);
413
+ }
414
+
415
+ function optionText(wrapper: RenderResult, selector: string): string[] {
416
+ return Array.from(control(wrapper, selector).querySelectorAll('option')).map((option) => option.textContent || '');
417
+ }
418
+
419
+ function optionValues(wrapper: RenderResult, selector: string): string[] {
420
+ return Array.from(control(wrapper, selector).querySelectorAll('option')).map((option) => option.value);
404
421
  }
405
422
 
406
- function expectValidationAlert(wrapper: any, id: string, message: string): void {
407
- const alert = wrapper.find(`#${id}`);
408
- expect(alert.prop('role')).withContext(id).toBe('alert');
409
- expect(alert.text()).withContext(id).toContain(message);
423
+ function control(wrapper: RenderResult, selector: string): HTMLElement {
424
+ const element = wrapper.container.querySelector<HTMLElement>(selector);
425
+ if (!element) {
426
+ throw new Error(`Missing control: ${selector}`);
427
+ }
428
+ return element;
410
429
  }
411
430
 
412
- function optionText(wrapper: any, selector: string): string[] {
413
- return wrapper.find(`${selector} option`).map((option: any) => option.text());
431
+ function renderWithRuntime(component: React.ReactElement, runtimeServices: any): RenderResult {
432
+ const container = document.body.appendChild(document.createElement('div'));
433
+ return render(
434
+ <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>{component}</DeckRuntimeContext.Provider>,
435
+ { baseElement: container, container },
436
+ );
414
437
  }
415
438
 
416
- function optionValues(wrapper: any, selector: string): string[] {
417
- return wrapper.find(`${selector} option`).map((option: any) => String(option.prop('value')));
439
+ function rerenderInstanceType(
440
+ rendered: RenderResult,
441
+ pageProps: IGceServerGroupWizardPageProps,
442
+ runtimeServices: any,
443
+ ): void {
444
+ rendered.rerender(
445
+ <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>
446
+ <ServerGroupInstanceType {...pageProps} />
447
+ </DeckRuntimeContext.Provider>,
448
+ );
418
449
  }
419
450
 
420
451
  function props(
@@ -425,24 +456,22 @@ function props(
425
456
  return {
426
457
  app: { name: 'app' } as any,
427
458
  adapter,
428
- formik: ({ errors, values, setValues: jasmine.createSpy('setValues') } as unknown) as FormikProps<
429
- IGceServerGroupCommand
430
- >,
459
+ formik: ({ errors, values, setValues: vi.fn() } as unknown) as FormikProps<IGceServerGroupCommand>,
431
460
  };
432
461
  }
433
462
 
434
463
  function adapterForHandlers(): IGceServerGroupWizardAdapter {
435
464
  return {
436
- configureCommand: jasmine.createSpy('configureCommand').and.callFake(async (_app, next) => next),
437
- applyCommandHandler: jasmine.createSpy('applyCommandHandler').and.callFake(async (next, handler) => ({
465
+ configureCommand: vi.fn().mockImplementation(async (_app, next) => next),
466
+ applyCommandHandler: vi.fn().mockImplementation(async (next, handler) => ({
438
467
  command: { ...next, handledBy: handler },
439
468
  result: { dirty: {} },
440
469
  })),
441
- applyConfigurationUpdate: jasmine.createSpy('applyConfigurationUpdate').and.callFake(async (next) => ({
470
+ applyConfigurationUpdate: vi.fn().mockImplementation(async (next) => ({
442
471
  command: next,
443
472
  result: { dirty: {} },
444
473
  })),
445
- applyConfigurationRefresh: jasmine.createSpy('applyConfigurationRefresh').and.callFake(async (next) => ({
474
+ applyConfigurationRefresh: vi.fn().mockImplementation(async (next) => ({
446
475
  command: next,
447
476
  result: { dirty: {} },
448
477
  })),
@@ -527,10 +556,11 @@ function instanceTypeDetails(name: string, diskSize: number): any {
527
556
  }
528
557
 
529
558
  function setFormikValues(
530
- wrapper: any,
559
+ wrapper: RenderResult,
531
560
  testProps: IGceServerGroupWizardPageProps,
532
561
  values: IGceServerGroupCommand,
562
+ runtimeServices: any,
533
563
  ): void {
534
564
  const formik = ({ ...testProps.formik, values } as unknown) as FormikProps<IGceServerGroupCommand>;
535
- wrapper.setProps({ formik });
565
+ rerenderInstanceType(wrapper, { ...testProps, formik }, runtimeServices);
536
566
  }
@@ -38,6 +38,34 @@ interface IGceInstanceTypeValidationErrors {
38
38
  instanceType?: string;
39
39
  }
40
40
 
41
+ export function validateGceServerGroupInstanceType(values: IGceServerGroupCommand): IGceInstanceTypeValidationErrors {
42
+ const errors: IGceInstanceTypeValidationErrors = {};
43
+ if (!values.instanceType) {
44
+ errors.instanceType = 'Machine type required.';
45
+ }
46
+
47
+ const bootDisk = getBootDisk(values);
48
+ if (!bootDisk?.type || !isValidBoundedValue(bootDisk.sizeGb, values, MIN_BOOT_DISK_SIZE_GB, MAX_BOOT_DISK_SIZE_GB)) {
49
+ errors.disks = `Boot disk size must be between ${MIN_BOOT_DISK_SIZE_GB} and ${MAX_BOOT_DISK_SIZE_GB} GB.`;
50
+ }
51
+
52
+ const accelerators = getAcceleratorConfigs(values);
53
+ const availableAccelerators = getAvailableAccelerators(values);
54
+ if (
55
+ accelerators.some((config) => {
56
+ if (!config.acceleratorType || !isValidPositiveInteger(config.acceleratorCount, values)) {
57
+ return true;
58
+ }
59
+ const available = availableAccelerators.find((accelerator) => accelerator.name === config.acceleratorType);
60
+ return available ? !available.availableCardCounts.includes(Number(config.acceleratorCount)) : false;
61
+ })
62
+ ) {
63
+ errors.acceleratorConfigs = 'Accelerator count is unavailable for the selected type.';
64
+ }
65
+
66
+ return errors;
67
+ }
68
+
41
69
  export class ServerGroupInstanceType extends GceServerGroupWizardPage {
42
70
  public static contextType = DeckRuntimeContext;
43
71
  public declare context: React.ContextType<typeof DeckRuntimeContext>;
@@ -45,34 +73,7 @@ export class ServerGroupInstanceType extends GceServerGroupWizardPage {
45
73
  private instanceTypeRequestGeneration = 0;
46
74
 
47
75
  public validate(values: IGceServerGroupCommand): IGceInstanceTypeValidationErrors {
48
- const errors: IGceInstanceTypeValidationErrors = {};
49
- if (!values.instanceType) {
50
- errors.instanceType = 'Machine type required.';
51
- }
52
-
53
- const bootDisk = getBootDisk(values);
54
- if (
55
- !bootDisk?.type ||
56
- !isValidBoundedValue(bootDisk.sizeGb, values, MIN_BOOT_DISK_SIZE_GB, MAX_BOOT_DISK_SIZE_GB)
57
- ) {
58
- errors.disks = `Boot disk size must be between ${MIN_BOOT_DISK_SIZE_GB} and ${MAX_BOOT_DISK_SIZE_GB} GB.`;
59
- }
60
-
61
- const accelerators = getAcceleratorConfigs(values);
62
- const availableAccelerators = getAvailableAccelerators(values);
63
- if (
64
- accelerators.some((config) => {
65
- if (!config.acceleratorType || !isValidPositiveInteger(config.acceleratorCount, values)) {
66
- return true;
67
- }
68
- const available = availableAccelerators.find((accelerator) => accelerator.name === config.acceleratorType);
69
- return available ? !available.availableCardCounts.includes(Number(config.acceleratorCount)) : false;
70
- })
71
- ) {
72
- errors.acceleratorConfigs = 'Accelerator count is unavailable for the selected type.';
73
- }
74
-
75
- return errors;
76
+ return validateGceServerGroupInstanceType(values);
76
77
  }
77
78
 
78
79
  private publish = (nextValues: IGceServerGroupCommand): void => {