@spinnaker/google 2026.3.1 → 2026.4.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 (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,16 +1,39 @@
1
- import { mount as enzymeMount } from 'enzyme';
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
2
  import React from 'react';
3
3
 
4
4
  import { DeckRuntimeContext } from '@spinnaker/core';
5
- import { CustomInstanceConfigurer } from './CustomInstanceConfigurer';
6
5
  import { GceCustomInstanceBuilder } from './GceCustomInstanceBuilder';
7
6
 
7
+ vi.mock('./CustomInstanceConfigurer', () => ({
8
+ CustomInstanceConfigurer: (props: any) => (
9
+ <div
10
+ data-testid="custom-instance-configurer"
11
+ data-extended={String(props.selectedExtendedMemory)}
12
+ data-family={props.selectedInstanceFamily}
13
+ data-memory={String(props.selectedMemory)}
14
+ data-vcpu={String(props.selectedVCpuCount)}
15
+ >
16
+ <span>{`families:${props.instanceFamilyList.join(',')}`}</span>
17
+ <span>{`cpus:${props.vCpuList.join(',')}`}</span>
18
+ <span>{`memory:${props.memoryList.join(',')}`}</span>
19
+ <button
20
+ onClick={() => props.onChange({ instanceFamily: 'N2', vCpuCount: 8, memory: 32, extendedMemory: false })}
21
+ type="button"
22
+ >
23
+ Select N2 custom type
24
+ </button>
25
+ <button
26
+ onClick={() => props.onChange({ instanceFamily: 'N1', vCpuCount: null, memory: 4, extendedMemory: false })}
27
+ type="button"
28
+ >
29
+ Select memory before cores
30
+ </button>
31
+ </div>
32
+ ),
33
+ }));
34
+
8
35
  describe('GceCustomInstanceBuilder', () => {
9
36
  let runtimeServices: any;
10
- const RuntimeWrapper = ({ children }: React.PropsWithChildren<{}>) => (
11
- <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>{children}</DeckRuntimeContext.Provider>
12
- );
13
- const mount = (component: React.ReactElement) => enzymeMount(component, { wrappingComponent: RuntimeWrapper });
14
37
 
15
38
  beforeEach(() => {
16
39
  runtimeServices = {};
@@ -20,49 +43,43 @@ describe('GceCustomInstanceBuilder', () => {
20
43
  runtimeServices.instanceTypeService = instanceTypeService();
21
44
  const command = commandWithCustomInstance('n2-custom-4-16384-ext');
22
45
 
23
- const component = mount(<GceCustomInstanceBuilder command={command as any} onTypeChanged={jasmine.createSpy()} />);
24
- const configurer = component.find(CustomInstanceConfigurer);
46
+ renderBuilder(command, vi.fn());
47
+ const configurer = screen.getByTestId('custom-instance-configurer');
25
48
 
26
- expect(configurer.prop('selectedInstanceFamily')).toBe('N2');
27
- expect(configurer.prop('selectedVCpuCount')).toBe(4);
28
- expect(configurer.prop('selectedMemory')).toBe(16);
29
- expect(configurer.prop('selectedExtendedMemory')).toBe(true);
30
- expect(configurer.prop('instanceFamilyList')).toContain('N2D');
31
- expect(configurer.prop('vCpuList')).toContain(4);
32
- expect(configurer.prop('memoryList')).toContain(16);
49
+ expect(configurer).toHaveAttribute('data-family', 'N2');
50
+ expect(configurer).toHaveAttribute('data-vcpu', '4');
51
+ expect(configurer).toHaveAttribute('data-memory', '16');
52
+ expect(configurer).toHaveAttribute('data-extended', 'true');
53
+ expect(screen.getByText(/families:.*N2D/)).toBeInTheDocument();
54
+ expect(screen.getByText(/cpus:.*4/)).toBeInTheDocument();
55
+ expect(screen.getByText(/memory:.*16/)).toBeInTheDocument();
33
56
  });
34
57
 
35
58
  it('updates command.instanceType, notifies, and loads details when custom choices change', async () => {
36
59
  const instanceTypeDetails = { name: 'n2-custom-8-32768' };
37
60
  runtimeServices.instanceTypeService = instanceTypeService(instanceTypeDetails);
38
- const onTypeChanged = jasmine.createSpy('onTypeChanged');
61
+ const onTypeChanged = vi.fn();
39
62
  const command = commandWithCustomInstance('n2-custom-4-16384');
63
+ renderBuilder(command, onTypeChanged);
40
64
 
41
- const component = mount(<GceCustomInstanceBuilder command={command as any} onTypeChanged={onTypeChanged} />);
42
- component.find(CustomInstanceConfigurer).prop('onChange')({
43
- instanceFamily: 'N2',
44
- vCpuCount: 8,
45
- memory: 32,
46
- extendedMemory: false,
47
- });
48
- await settle(component);
65
+ fireEvent.click(screen.getByRole('button', { name: 'Select N2 custom type' }));
49
66
 
50
67
  expect(command.instanceType).toBe('n2-custom-8-32768');
51
68
  expect(command.customInstanceChanged).toHaveBeenCalledWith(command);
52
69
  expect(onTypeChanged).toHaveBeenCalledWith('n2-custom-8-32768');
53
- expect(command.viewState.instanceTypeDetails).toBe(instanceTypeDetails as any);
70
+ await waitFor(() => expect(command.viewState.instanceTypeDetails).toBe(instanceTypeDetails));
54
71
  });
55
72
 
56
73
  it('initializes missing custom values from valid lists', () => {
57
74
  runtimeServices.instanceTypeService = instanceTypeService();
58
75
  const command = commandWithCustomInstance(null);
59
76
 
60
- const component = mount(<GceCustomInstanceBuilder command={command as any} onTypeChanged={jasmine.createSpy()} />);
61
- const configurer = component.find(CustomInstanceConfigurer);
77
+ renderBuilder(command, vi.fn());
78
+ const configurer = screen.getByTestId('custom-instance-configurer');
62
79
 
63
- expect(configurer.prop('selectedInstanceFamily')).toBe('N1');
64
- expect(configurer.prop('selectedVCpuCount')).toBe(1);
65
- expect(configurer.prop('selectedMemory')).toBe(1);
80
+ expect(configurer).toHaveAttribute('data-family', 'N1');
81
+ expect(configurer).toHaveAttribute('data-vcpu', '1');
82
+ expect(configurer).toHaveAttribute('data-memory', '1');
66
83
  expect(command.viewState.customInstance).toEqual({
67
84
  extendedMemory: false,
68
85
  instanceFamily: 'N1',
@@ -71,23 +88,28 @@ describe('GceCustomInstanceBuilder', () => {
71
88
  });
72
89
  });
73
90
 
74
- it('keeps valid defaults when memory changes before cores', async () => {
91
+ it('keeps valid defaults when memory changes before cores', () => {
75
92
  runtimeServices.instanceTypeService = instanceTypeService();
76
- const onTypeChanged = jasmine.createSpy('onTypeChanged');
93
+ const onTypeChanged = vi.fn();
77
94
  const command = commandWithCustomInstance(null);
95
+ renderBuilder(command, onTypeChanged);
78
96
 
79
- const component = mount(<GceCustomInstanceBuilder command={command as any} onTypeChanged={onTypeChanged} />);
80
- component.find(CustomInstanceConfigurer).prop('onChange')({
81
- instanceFamily: 'N1',
82
- vCpuCount: null,
83
- memory: 4,
84
- extendedMemory: false,
85
- } as any);
86
- await settle(component);
97
+ fireEvent.click(screen.getByRole('button', { name: 'Select memory before cores' }));
87
98
 
88
99
  expect(command.instanceType).toBe('custom-1-4096');
89
100
  expect(onTypeChanged).toHaveBeenCalledWith('custom-1-4096');
90
101
  });
102
+
103
+ function renderBuilder(
104
+ command: ReturnType<typeof commandWithCustomInstance>,
105
+ onTypeChanged: ReturnType<typeof vi.fn>,
106
+ ) {
107
+ return render(
108
+ <DeckRuntimeContext.Provider value={{ services: runtimeServices } as any}>
109
+ <GceCustomInstanceBuilder command={command as any} onTypeChanged={onTypeChanged} />
110
+ </DeckRuntimeContext.Provider>,
111
+ );
112
+ }
91
113
  });
92
114
 
93
115
  function commandWithCustomInstance(instanceType: string | null) {
@@ -96,7 +118,7 @@ function commandWithCustomInstance(instanceType: string | null) {
96
118
  instanceType,
97
119
  region: 'us-central1',
98
120
  regional: true,
99
- viewState: {},
121
+ viewState: {} as any,
100
122
  backingData: {
101
123
  customInstanceTypes: {},
102
124
  credentialsKeyedByAccount: {
@@ -108,18 +130,12 @@ function commandWithCustomInstance(instanceType: string | null) {
108
130
  },
109
131
  },
110
132
  credentials: 'test',
111
- customInstanceChanged: jasmine.createSpy('customInstanceChanged'),
133
+ customInstanceChanged: vi.fn(),
112
134
  };
113
135
  }
114
136
 
115
137
  function instanceTypeService(details = { name: 'n2-custom-8-32768' }) {
116
138
  return {
117
- getInstanceTypeDetails: jasmine.createSpy('getInstanceTypeDetails').and.returnValue(Promise.resolve(details)),
139
+ getInstanceTypeDetails: vi.fn().mockResolvedValue(details),
118
140
  };
119
141
  }
120
-
121
- async function settle(component: any) {
122
- await Promise.resolve();
123
- await Promise.resolve();
124
- component.update();
125
- }
@@ -2,6 +2,7 @@
2
2
 
3
3
  export const GCE_SERVER_GROUP_HIDDEN_METADATA_KEYS = [
4
4
  'load-balancer-names',
5
+ 'regional-external-load-balancer-names',
5
6
  'global-load-balancer-names',
6
7
  'backend-service-names',
7
8
  'load-balancing-policy',
@@ -1,48 +1,38 @@
1
- import { shallow } from 'enzyme';
1
+ import { fireEvent, render, screen, within } from '@testing-library/react';
2
2
  import React from 'react';
3
3
 
4
- import { MapEditor } from '@spinnaker/core';
5
-
6
- import { AdvancedSettings } from './AdvancedSettings';
4
+ import { AdvancedSettings, validateGceAdvancedSettings } from './AdvancedSettings';
7
5
  import type { IGceServerGroupCommand } from '../GceServerGroupWizard.types';
8
6
 
9
7
  describe('GCE server group Advanced Settings page', () => {
10
8
  it('restores persisted advanced fields and preserves unknown options and maps', () => {
11
9
  const values = command();
12
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
13
-
14
- expect(wrapper.find('[data-testid="minimum-cpu-platform"] option').map((option) => option.prop('value'))).toEqual([
15
- 'Automatic',
16
- 'legacy-platform',
17
- ]);
18
- expect(wrapper.find('[data-testid="disk-type-0"] option').map((option) => option.prop('value'))).toEqual([
19
- 'pd-ssd',
20
- 'legacy-disk',
21
- ]);
22
- expect(wrapper.find('[data-testid="accelerator-type-0"] option').map((option) => option.prop('value'))).toEqual([
23
- 'nvidia-tesla-t4',
24
- 'legacy-accelerator',
25
- ]);
26
- expect(wrapper.find('[data-testid="service-account"] input').prop('value')).toBe('custom@example.com');
27
- expect(wrapper.find('[data-testid="auth-scope-0"] input').prop('value')).toBe('unknown.scope');
28
- expect(wrapper.find('[data-testid="enable-confidential-compute"] input').prop('checked')).toBe(true);
29
-
30
- const editors = wrapper.find(MapEditor);
31
- expect(editors.at(0).prop('model')).toBe(values.instanceMetadata);
32
- expect(editors.at(1).prop('model')).toBe(values.labels);
33
- expect(editors.at(2).prop('model')).toBe(values.resourceManagerTags);
10
+ renderAdvanced(values);
11
+
12
+ expect(optionValues(screen.getByTestId('minimum-cpu-platform'))).toEqual(['Automatic', 'legacy-platform']);
13
+ expect(optionValues(screen.getByTestId('disk-type-0'))).toEqual(['pd-ssd', 'legacy-disk']);
14
+ expect(optionValues(screen.getByTestId('accelerator-type-0'))).toEqual(['nvidia-tesla-t4', 'legacy-accelerator']);
15
+ expect(screen.getByTestId('service-account').querySelector('input')).toHaveValue('custom@example.com');
16
+ expect(screen.getByTestId('auth-scope-0').querySelector('input')).toHaveValue('unknown.scope');
17
+ expect(screen.getByTestId('enable-confidential-compute').querySelector('input')).toBeChecked();
18
+ expect(mapValues('Custom Metadata')).toEqual(['unknownMetadata', 'keep']);
19
+ expect(mapValues('Labels')).toEqual(['unknownLabel', 'keep']);
20
+ expect(mapValues('Resource Manager Tags')).toEqual(['unknownTag', 'keep']);
34
21
  });
35
22
 
36
23
  it('preserves unknown disk and accelerator fields while editing known values', () => {
37
- const values = command();
38
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
24
+ // A persisted legacy accelerator only offers its stored count, so edit a known type with selectable counts.
25
+ const values = command({
26
+ acceleratorConfigs: [{ acceleratorType: 'nvidia-tesla-t4', acceleratorCount: 2, unknown: 'keep' }],
27
+ });
28
+ renderAdvanced(values);
39
29
 
40
- wrapper.find('[data-testid="disk-size-0"]').simulate('change', { target: { value: '200' } });
41
- wrapper.find('[data-testid="accelerator-count-0"]').simulate('change', { target: { value: '4' } });
30
+ fireEvent.change(screen.getByTestId('disk-size-0'), { target: { value: '200' } });
31
+ fireEvent.change(screen.getByTestId('accelerator-count-0'), { target: { value: '4' } });
42
32
 
43
33
  expect(values.disks[0]).toEqual({ type: 'legacy-disk', sizeGb: 200, sourceImage: 'image', unknown: 'keep' });
44
34
  expect(values.acceleratorConfigs[0]).toEqual({
45
- acceleratorType: 'legacy-accelerator',
35
+ acceleratorType: 'nvidia-tesla-t4',
46
36
  acceleratorCount: 4,
47
37
  unknown: 'keep',
48
38
  });
@@ -52,16 +42,16 @@ describe('GCE server group Advanced Settings page', () => {
52
42
  const firstLocalSsd = { type: 'local-ssd', sizeGb: 375, autoDelete: true, unknown: 'first' };
53
43
  const secondLocalSsd = { type: 'local-ssd', sizeGb: 375, autoDelete: true, unknown: 'second' };
54
44
  const values = command({ disks: [command().disks[0], firstLocalSsd, secondLocalSsd] });
55
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
45
+ renderAdvanced(values);
56
46
 
57
- expect(wrapper.find('[data-testid="local-ssd-count"]').prop('value')).toBe(2);
58
- expect(wrapper.find('[data-testid^="disk-type-"]').length).toBe(1);
47
+ expect(screen.getByTestId('local-ssd-count')).toHaveValue(2);
48
+ expect(screen.getAllByTestId(/^disk-type-/)).toHaveLength(1);
59
49
 
60
- wrapper.find('[data-testid="local-ssd-count"]').simulate('change', { target: { value: '3' } });
50
+ fireEvent.change(screen.getByTestId('local-ssd-count'), { target: { value: '3' } });
61
51
  expect(values.disks.slice(1, 3)).toEqual([firstLocalSsd, secondLocalSsd]);
62
52
  expect(values.disks[3]).toEqual({ type: 'local-ssd', sizeGb: 375 });
63
53
 
64
- wrapper.find('[data-testid="local-ssd-count"]').simulate('change', { target: { value: '1' } });
54
+ fireEvent.change(screen.getByTestId('local-ssd-count'), { target: { value: '1' } });
65
55
  expect(values.disks).toEqual([command().disks[0], firstLocalSsd]);
66
56
  });
67
57
 
@@ -73,33 +63,30 @@ describe('GCE server group Advanced Settings page', () => {
73
63
  { acceleratorType: 'nvidia-tesla-t4', acceleratorCount: '${ parameters.acceleratorCount }' },
74
64
  ],
75
65
  });
76
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
66
+ renderAdvanced(values);
77
67
 
78
- expect(wrapper.find('[data-testid="disk-size-0"]').prop('type')).toBe('text');
79
- expect(wrapper.find('[data-testid="accelerator-count-0"]').prop('type')).toBe('text');
80
- wrapper.find('[data-testid="disk-size-0"]').simulate('change', { target: { value: '${ diskSize }' } });
81
- wrapper
82
- .find('[data-testid="accelerator-count-0"]')
83
- .simulate('change', { target: { value: '${ acceleratorCount }' } });
68
+ expect(screen.getByTestId('disk-size-0')).toHaveAttribute('type', 'text');
69
+ expect(screen.getByTestId('accelerator-count-0')).toHaveAttribute('type', 'text');
70
+ fireEvent.change(screen.getByTestId('disk-size-0'), { target: { value: '${ diskSize }' } });
71
+ fireEvent.change(screen.getByTestId('accelerator-count-0'), { target: { value: '${ acceleratorCount }' } });
84
72
 
85
73
  expect(values.disks[0].sizeGb).toBe('${ diskSize }');
86
74
  expect(values.acceleratorConfigs[0].acceleratorCount).toBe('${ acceleratorCount }');
87
- expect(new AdvancedSettings({ app: {} as any, formik: formik(values) } as any).validate(values)).toEqual({});
75
+ expect(validateGceAdvancedSettings(values)).toEqual({});
88
76
  });
89
77
 
90
78
  it('enforces concrete disk and accelerator bounds and rejects expressions outside pipelines', () => {
91
- const page = new AdvancedSettings({ app: {} as any, formik: formik(command()) } as any);
92
79
  const invalid = command({
93
80
  disks: [{ type: 'pd-ssd', sizeGb: 9 }],
94
81
  acceleratorConfigs: [{ acceleratorType: 'nvidia-tesla-t4', acceleratorCount: 0 }],
95
82
  });
96
83
 
97
- expect(page.validate(invalid)).toEqual({
84
+ expect(validateGceAdvancedSettings(invalid)).toEqual({
98
85
  disks: 'Every persistent disk requires a type and an integer size between 10 and 65536 GB.',
99
86
  acceleratorConfigs: 'Every accelerator requires a type and a supported positive integer count.',
100
87
  });
101
88
  expect(
102
- page.validate(
89
+ validateGceAdvancedSettings(
103
90
  command({
104
91
  disks: [{ type: 'pd-ssd', sizeGb: 65537 }],
105
92
  acceleratorConfigs: [{ acceleratorType: 'nvidia-tesla-t4', acceleratorCount: 3 }],
@@ -110,7 +97,7 @@ describe('GCE server group Advanced Settings page', () => {
110
97
  acceleratorConfigs: 'Every accelerator requires a type and a supported positive integer count.',
111
98
  });
112
99
  expect(
113
- page.validate(
100
+ validateGceAdvancedSettings(
114
101
  command({
115
102
  disks: [{ type: 'pd-ssd', sizeGb: '${ diskSize }' }],
116
103
  acceleratorConfigs: [{ acceleratorType: 'nvidia-tesla-t4', acceleratorCount: '${ count }' }],
@@ -121,19 +108,18 @@ describe('GCE server group Advanced Settings page', () => {
121
108
  acceleratorConfigs: 'Every accelerator requires a type and a supported positive integer count.',
122
109
  });
123
110
 
124
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(command())} />);
125
- expect(wrapper.find('[data-testid="disk-size-0"]').prop('min')).toBe(10);
126
- expect(wrapper.find('[data-testid="disk-size-0"]').prop('max')).toBe(65536);
111
+ renderAdvanced(command());
112
+ expect(screen.getByTestId('disk-size-0')).toHaveAttribute('min', '10');
113
+ expect(screen.getByTestId('disk-size-0')).toHaveAttribute('max', '65536');
127
114
  });
128
115
 
129
116
  it('does not render or validate the unsupported partner metadata editor', () => {
130
117
  const values = command({ partnerMetadata: { partner: { entries: { unknown: 'keep' } } } });
131
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
132
- const page = new AdvancedSettings({ app: {} as any, formik: formik(values) } as any);
118
+ const { container } = renderAdvanced(values);
133
119
 
134
- expect(wrapper.find('[data-testid="partner-metadata"]').exists()).toBe(false);
135
- expect(wrapper.text()).not.toContain('Partner Metadata');
136
- expect(page.validate(command({ partnerMetadata: '{legacy-invalid-json' }))).toEqual({});
120
+ expect(screen.queryByTestId('partner-metadata')).not.toBeInTheDocument();
121
+ expect(container).not.toHaveTextContent('Partner Metadata');
122
+ expect(validateGceAdvancedSettings(command({ partnerMetadata: '{legacy-invalid-json' }))).toEqual({});
137
123
  });
138
124
 
139
125
  it('keeps scheduling and shielded VM constraints internally consistent', () => {
@@ -144,43 +130,44 @@ describe('GCE server group Advanced Settings page', () => {
144
130
  enableVtpm: true,
145
131
  enableIntegrityMonitoring: true,
146
132
  });
147
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
133
+ const page = renderAdvanced(values);
148
134
 
149
- wrapper.find('[data-testid="preemptible-on"]').simulate('change');
135
+ fireEvent.click(screen.getByTestId('preemptible-on'));
150
136
  expect(values.preemptible).toBe(true);
151
137
  expect(values.automaticRestart).toBe(false);
152
138
  expect(values.onHostMaintenance).toBe('TERMINATE');
153
139
 
154
- wrapper.find('[data-testid="preemptible-off"]').simulate('change');
140
+ page.rerenderPage(values);
141
+ fireEvent.click(screen.getByTestId('preemptible-off'));
155
142
  expect(values.preemptible).toBe(false);
156
143
  expect(values.automaticRestart).toBe(true);
157
144
  expect(values.onHostMaintenance).toBe('MIGRATE');
158
145
 
159
- wrapper.find('[data-testid="enable-vtpm"]').simulate('change', { target: { checked: false } });
146
+ fireEvent.click(screen.getByTestId('enable-vtpm'));
160
147
  expect(values.enableVtpm).toBe(false);
161
148
  expect(values.enableIntegrityMonitoring).toBe(false);
162
149
  });
163
150
 
164
151
  it('uses accessible non-submit controls for every add and remove action', () => {
165
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(command())} />);
166
- const actionButtons = wrapper.find('button');
152
+ const { container } = renderAdvanced(command());
153
+ const actionButtons = Array.from(container.querySelectorAll('button'));
167
154
 
168
155
  expect(actionButtons.length).toBeGreaterThan(0);
169
156
  actionButtons.forEach((button) => {
170
- expect(button.prop('type')).toBe('button');
171
- expect(button.prop('aria-label') || button.text().trim()).toBeTruthy();
157
+ expect(button).toHaveAttribute('type', 'button');
158
+ expect(button.getAttribute('aria-label') || button.textContent?.trim()).toBeTruthy();
172
159
  });
173
160
  });
174
161
 
175
162
  it('adds and removes disks, accelerators, tags, and custom scopes without mutating existing entries', () => {
176
163
  const values = command();
177
164
  const originalDisk = values.disks[0];
178
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
165
+ renderAdvanced(values);
179
166
 
180
- wrapper.find('[data-testid="add-disk"]').simulate('click');
181
- wrapper.find('[data-testid="add-accelerator"]').simulate('click');
182
- wrapper.find('[data-testid="add-network-tag"]').simulate('click');
183
- wrapper.find('[data-testid="add-auth-scope"]').simulate('click');
167
+ fireEvent.click(screen.getByTestId('add-disk'));
168
+ fireEvent.click(screen.getByTestId('add-accelerator'));
169
+ fireEvent.click(screen.getByTestId('add-network-tag'));
170
+ fireEvent.click(screen.getByTestId('add-auth-scope'));
184
171
 
185
172
  expect(values.disks.length).toBe(2);
186
173
  expect(values.disks[0]).toBe(originalDisk);
@@ -188,10 +175,10 @@ describe('GCE server group Advanced Settings page', () => {
188
175
  expect(values.tags).toEqual([{ value: 'existing-tag', unknown: 'keep' }, { value: '' }]);
189
176
  expect(values.authScopes).toEqual(['unknown.scope', '']);
190
177
 
191
- wrapper.find('[data-testid="remove-disk-0"]').simulate('click');
192
- wrapper.find('[data-testid="remove-accelerator-0"]').simulate('click');
193
- wrapper.find('[data-testid="remove-network-tag-0"]').simulate('click');
194
- wrapper.find('[data-testid="remove-auth-scope-0"]').simulate('click');
178
+ fireEvent.click(screen.getByTestId('remove-disk-0'));
179
+ fireEvent.click(screen.getByTestId('remove-accelerator-0'));
180
+ fireEvent.click(screen.getByTestId('remove-network-tag-0'));
181
+ fireEvent.click(screen.getByTestId('remove-auth-scope-0'));
195
182
 
196
183
  expect(values.disks.length).toBe(1);
197
184
  expect(values.acceleratorConfigs.length).toBe(1);
@@ -200,10 +187,8 @@ describe('GCE server group Advanced Settings page', () => {
200
187
  });
201
188
 
202
189
  it('validates invalid disks, accelerators, maps, tags, scopes, and confidential settings', () => {
203
- const page = new AdvancedSettings({ app: {} as any, formik: formik(command()) } as any);
204
-
205
190
  expect(
206
- page.validate(
191
+ validateGceAdvancedSettings(
207
192
  command({
208
193
  disks: [{ type: '', sizeGb: 0 }],
209
194
  acceleratorConfigs: [{ acceleratorType: '', acceleratorCount: 0 }],
@@ -240,7 +225,7 @@ describe('GCE server group Advanced Settings page', () => {
240
225
  enableConfidentialCompute: true,
241
226
  confidentialInstanceType: '',
242
227
  });
243
- const wrapper = shallow(<AdvancedSettings app={{} as any} formik={formik(values)} />);
228
+ const { container } = renderAdvanced(values);
244
229
 
245
230
  [
246
231
  ['[data-testid="local-ssd-count"]', 'gce-advanced-disks-error'],
@@ -255,34 +240,48 @@ describe('GCE server group Advanced Settings page', () => {
255
240
  ['#gce-confidential-instance-type', 'gce-advanced-confidential-instance-type-error'],
256
241
  ['[aria-label="Auth scope 1"]', 'gce-advanced-auth-scopes-error'],
257
242
  ].forEach(([selector, errorId]) => {
258
- const control = wrapper.find(selector);
259
- expect(control.prop('aria-invalid')).withContext(selector).toBe(true);
260
- expect(control.prop('aria-describedby')).withContext(selector).toBe(errorId);
243
+ const control = container.querySelector(selector);
244
+ expect(control, selector).toHaveAttribute('aria-invalid', 'true');
245
+ expect(control, selector).toHaveAttribute('aria-describedby', errorId);
261
246
  });
262
247
 
263
- expectAdvancedError(wrapper, 'gce-advanced-disks-error', 'Every persistent disk requires a type');
264
- expectAdvancedError(wrapper, 'gce-advanced-accelerators-error', 'Every accelerator requires a type');
265
- expectAdvancedError(wrapper, 'gce-advanced-instance-metadata-error', 'Metadata keys cannot be empty.');
266
- expectAdvancedError(wrapper, 'gce-advanced-labels-error', 'Label values cannot be empty.');
267
- expectAdvancedError(
268
- wrapper,
269
- 'gce-advanced-resource-manager-tags-error',
270
- 'Resource Manager tag values cannot be empty.',
271
- );
272
- expectAdvancedError(wrapper, 'gce-advanced-network-tags-error', 'Network tags cannot be empty.');
273
- expectAdvancedError(
274
- wrapper,
275
- 'gce-advanced-confidential-instance-type-error',
276
- 'Confidential instance type required.',
277
- );
278
- expectAdvancedError(wrapper, 'gce-advanced-auth-scopes-error', 'Auth scopes cannot be empty.');
248
+ expectAdvancedError('gce-advanced-disks-error', 'Every persistent disk requires a type');
249
+ expectAdvancedError('gce-advanced-accelerators-error', 'Every accelerator requires a type');
250
+ expectAdvancedError('gce-advanced-instance-metadata-error', 'Metadata keys cannot be empty.');
251
+ expectAdvancedError('gce-advanced-labels-error', 'Label values cannot be empty.');
252
+ expectAdvancedError('gce-advanced-resource-manager-tags-error', 'Resource Manager tag values cannot be empty.');
253
+ expectAdvancedError('gce-advanced-network-tags-error', 'Network tags cannot be empty.');
254
+ expectAdvancedError('gce-advanced-confidential-instance-type-error', 'Confidential instance type required.');
255
+ expectAdvancedError('gce-advanced-auth-scopes-error', 'Auth scopes cannot be empty.');
279
256
  });
280
257
  });
281
258
 
282
- function expectAdvancedError(wrapper: ReturnType<typeof shallow>, id: string, message: string): void {
283
- const error = wrapper.find(`#${id}`);
284
- expect(error.prop('role')).withContext(id).toBe('alert');
285
- expect(error.text()).withContext(id).toContain(message);
259
+ function renderAdvanced(values: IGceServerGroupCommand) {
260
+ const pageProps = { app: {} as any, formik: formik(values) };
261
+ const rendered = render(<AdvancedSettings {...pageProps} />);
262
+ return {
263
+ ...rendered,
264
+ rerenderPage(nextValues: IGceServerGroupCommand) {
265
+ rendered.rerender(<AdvancedSettings {...pageProps} formik={formik(nextValues)} />);
266
+ },
267
+ };
268
+ }
269
+
270
+ function optionValues(select: HTMLElement): string[] {
271
+ return within(select)
272
+ .getAllByRole('option')
273
+ .map((option) => (option as HTMLOptionElement).value);
274
+ }
275
+
276
+ function mapValues(label: string): string[] {
277
+ const editor = document.querySelector(`[aria-label="${label}"]`)!;
278
+ return Array.from(editor.querySelectorAll('input')).map((input) => input.value);
279
+ }
280
+
281
+ function expectAdvancedError(id: string, message: string): void {
282
+ const error = document.getElementById(id);
283
+ expect(error, id).toHaveAttribute('role', 'alert');
284
+ expect(error, id).toHaveTextContent(message);
286
285
  }
287
286
 
288
287
  function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGroupCommand {
@@ -292,13 +291,7 @@ function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGro
292
291
  viewState: {
293
292
  mode: 'clone',
294
293
  instanceTypeDetails: { storage: { localSSDSupported: true } },
295
- acceleratorTypes: [
296
- {
297
- name: 'nvidia-tesla-t4',
298
- description: 'NVIDIA Tesla T4',
299
- availableCardCounts: [1, 2, 4],
300
- },
301
- ],
294
+ acceleratorTypes: [{ name: 'nvidia-tesla-t4', description: 'NVIDIA Tesla T4', availableCardCounts: [1, 2, 4] }],
302
295
  },
303
296
  backingData: {
304
297
  persistentDiskTypes: ['pd-ssd'],
@@ -332,7 +325,7 @@ function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGro
332
325
  function formik(values: IGceServerGroupCommand): any {
333
326
  return {
334
327
  values,
335
- setFieldValue: jasmine.createSpy('setFieldValue').and.callFake((field: string, value: any) => {
328
+ setFieldValue: vi.fn().mockImplementation((field: string, value: any) => {
336
329
  values[field] = value;
337
330
  }),
338
331
  };
@@ -80,34 +80,7 @@ function renderValidationError(error: string, id: string): React.ReactNode {
80
80
 
81
81
  export class AdvancedSettings extends GceServerGroupWizardPage {
82
82
  public validate(values: IGceServerGroupCommand): { [key: string]: string } {
83
- const errors: { [key: string]: string } = {};
84
- if (getPersistentDisks(values).some((disk: any) => !disk.type || !isValidInteger(disk.sizeGb, 10, values, 65536))) {
85
- errors.disks = 'Every persistent disk requires a type and an integer size between 10 and 65536 GB.';
86
- }
87
- if ((values.acceleratorConfigs || []).some((accelerator: any) => !isValidAccelerator(accelerator, values))) {
88
- errors.acceleratorConfigs = 'Every accelerator requires a type and a supported positive integer count.';
89
- }
90
- if (hasEmptyKey(values.instanceMetadata)) {
91
- errors.instanceMetadata = 'Metadata keys cannot be empty.';
92
- }
93
- if (hasEmptyKey(values.labels) || hasEmptyValue(values.labels)) {
94
- errors.labels = hasEmptyKey(values.labels) ? 'Label keys cannot be empty.' : 'Label values cannot be empty.';
95
- }
96
- if (hasEmptyKey(values.resourceManagerTags) || hasEmptyValue(values.resourceManagerTags)) {
97
- errors.resourceManagerTags = hasEmptyKey(values.resourceManagerTags)
98
- ? 'Resource Manager tag keys cannot be empty.'
99
- : 'Resource Manager tag values cannot be empty.';
100
- }
101
- if ((values.tags || []).some((tag: any) => !(typeof tag === 'string' ? tag : tag.value)?.trim())) {
102
- errors.tags = 'Network tags cannot be empty.';
103
- }
104
- if ((values.authScopes || []).some((scope: string) => !scope.trim())) {
105
- errors.authScopes = 'Auth scopes cannot be empty.';
106
- }
107
- if (values.enableConfidentialCompute && 'confidentialInstanceType' in values && !values.confidentialInstanceType) {
108
- errors.confidentialInstanceType = 'Confidential instance type required.';
109
- }
110
- return errors;
83
+ return validateGceAdvancedSettings(values);
111
84
  }
112
85
 
113
86
  private setField = (field: string, value: any): void => {
@@ -869,3 +842,34 @@ export class AdvancedSettings extends GceServerGroupWizardPage {
869
842
  );
870
843
  }
871
844
  }
845
+
846
+ export function validateGceAdvancedSettings(values: IGceServerGroupCommand): { [key: string]: string } {
847
+ const errors: { [key: string]: string } = {};
848
+ if (getPersistentDisks(values).some((disk: any) => !disk.type || !isValidInteger(disk.sizeGb, 10, values, 65536))) {
849
+ errors.disks = 'Every persistent disk requires a type and an integer size between 10 and 65536 GB.';
850
+ }
851
+ if ((values.acceleratorConfigs || []).some((accelerator: any) => !isValidAccelerator(accelerator, values))) {
852
+ errors.acceleratorConfigs = 'Every accelerator requires a type and a supported positive integer count.';
853
+ }
854
+ if (hasEmptyKey(values.instanceMetadata)) {
855
+ errors.instanceMetadata = 'Metadata keys cannot be empty.';
856
+ }
857
+ if (hasEmptyKey(values.labels) || hasEmptyValue(values.labels)) {
858
+ errors.labels = hasEmptyKey(values.labels) ? 'Label keys cannot be empty.' : 'Label values cannot be empty.';
859
+ }
860
+ if (hasEmptyKey(values.resourceManagerTags) || hasEmptyValue(values.resourceManagerTags)) {
861
+ errors.resourceManagerTags = hasEmptyKey(values.resourceManagerTags)
862
+ ? 'Resource Manager tag keys cannot be empty.'
863
+ : 'Resource Manager tag values cannot be empty.';
864
+ }
865
+ if ((values.tags || []).some((tag: any) => !(typeof tag === 'string' ? tag : tag.value)?.trim())) {
866
+ errors.tags = 'Network tags cannot be empty.';
867
+ }
868
+ if ((values.authScopes || []).some((scope: string) => !scope.trim())) {
869
+ errors.authScopes = 'Auth scopes cannot be empty.';
870
+ }
871
+ if (values.enableConfidentialCompute && 'confidentialInstanceType' in values && !values.confidentialInstanceType) {
872
+ errors.confidentialInstanceType = 'Confidential instance type required.';
873
+ }
874
+ return errors;
875
+ }