@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,30 +1,35 @@
1
- import { shallow } from 'enzyme';
1
+ import { fireEvent, render } from '@testing-library/react';
2
+ import type { RenderResult } from '@testing-library/react';
2
3
  import type { FormikProps } from 'formik';
3
4
  import React from 'react';
5
+ import type { Mocked } from 'vitest';
4
6
 
5
- import { GceServerGroupLoadBalancers } from './GceServerGroupLoadBalancers';
7
+ import { GceServerGroupLoadBalancers, validateGceServerGroupLoadBalancers } from './GceServerGroupLoadBalancers';
6
8
  import type { IGceServerGroupCommand, IGceServerGroupWizardAdapter } from '../GceServerGroupWizard.types';
7
9
 
8
10
  describe('GCE server group Load Balancers page', () => {
9
11
  it('shows only account and region scoped load balancers without duplicate references', () => {
10
12
  const values = command({ loadBalancers: ['regional-lb', 'persisted-lb', 'persisted-lb'] });
11
13
  const { formik } = testProps(values);
12
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
14
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
13
15
 
14
16
  expect(selectOptions(wrapper)).toEqual([
15
17
  ['global-lb', 'global-lb'],
16
18
  ['regional-lb', 'regional-lb'],
17
19
  ['persisted-lb', 'persisted-lb (unavailable)'],
18
20
  ]);
19
- expect(wrapper.find('label[htmlFor="gce-server-group-load-balancers"]').text()).toBe('Load Balancers');
20
- expect(wrapper.find('select[aria-label="Load balancers"]').prop('value')).toEqual(['regional-lb', 'persisted-lb']);
21
+ expect(wrapper.getByText('Load Balancers', { selector: 'label' })).toHaveAttribute(
22
+ 'for',
23
+ 'gce-server-group-load-balancers',
24
+ );
25
+ expect(selectedValues(wrapper.getByLabelText('Load balancers'))).toEqual(['regional-lb', 'persisted-lb']);
21
26
  });
22
27
 
23
28
  it('does not fall back to stale filtered load balancers when raw data has no scoped matches', () => {
24
29
  const values = command({ credentials: 'account-c' });
25
30
  values.backingData.filtered.loadBalancers = ['stale-lb'];
26
31
  const { formik } = testProps(values);
27
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
32
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
28
33
 
29
34
  expect(selectOptions(wrapper)).toEqual([]);
30
35
  });
@@ -32,22 +37,20 @@ describe('GCE server group Load Balancers page', () => {
32
37
  it('runs load-balancer configuration and preserves selected unavailable references', async () => {
33
38
  const values = command({ loadBalancers: ['persisted-lb'] });
34
39
  const { adapter, formik } = testProps(values);
35
- adapter.applyConfigurationUpdate.and.callFake(async (nextCommand) => ({
40
+ adapter.applyConfigurationUpdate.mockImplementation(async (nextCommand) => ({
36
41
  command: { ...nextCommand, loadBalancers: ['regional-lb'], backendServices: { 'regional-lb': ['backend'] } },
37
42
  result: { dirty: { loadBalancers: ['persisted-lb'] } },
38
43
  }));
39
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
44
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
40
45
 
41
- wrapper.find('select[aria-label="Load balancers"]').simulate('change', {
42
- target: { selectedOptions: [{ value: 'regional-lb' }, { value: 'persisted-lb' }, { value: 'regional-lb' }] },
43
- });
46
+ changeMultiSelect(wrapper.getByLabelText('Load balancers'), ['regional-lb', 'persisted-lb']);
44
47
  await flush();
45
48
 
46
- const changedCommand = adapter.applyConfigurationUpdate.calls.mostRecent().args[0];
49
+ const changedCommand = adapter.applyConfigurationUpdate.mock.lastCall[0];
47
50
  expect(changedCommand.loadBalancers).toEqual(['regional-lb']);
48
51
  expect(adapter.applyConfigurationUpdate).toHaveBeenCalledWith(changedCommand, 'configureLoadBalancerOptions');
49
52
  expect(formik.setValues).toHaveBeenCalledWith(
50
- jasmine.objectContaining({
53
+ expect.objectContaining({
51
54
  backendServices: { 'regional-lb': ['backend'] },
52
55
  loadBalancers: ['regional-lb', 'persisted-lb'],
53
56
  }),
@@ -57,25 +60,23 @@ describe('GCE server group Load Balancers page', () => {
57
60
  it('initializes the backend-compatible default policy when selecting the first supported load balancer', async () => {
58
61
  const values = command();
59
62
  const { adapter, formik } = testProps(values);
60
- adapter.applyConfigurationUpdate.and.callFake(async (nextCommand) => ({
63
+ adapter.applyConfigurationUpdate.mockImplementation(async (nextCommand) => ({
61
64
  command: { ...nextCommand, loadBalancingPolicy: undefined },
62
65
  result: { dirty: {} },
63
66
  }));
64
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
67
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
65
68
 
66
- wrapper.find('select[aria-label="Load balancers"]').simulate('change', {
67
- target: { selectedOptions: [{ value: 'regional-lb' }] },
68
- });
69
+ changeMultiSelect(wrapper.getByLabelText('Load balancers'), ['regional-lb']);
69
70
  await flush();
70
71
 
71
- expect(adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancingPolicy).toEqual({
72
+ expect(adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancingPolicy).toEqual({
72
73
  balancingMode: 'UTILIZATION',
73
74
  capacityScaler: 1,
74
75
  maxUtilization: 0.8,
75
76
  namedPorts: [{ name: 'http', port: 80 }],
76
77
  });
77
78
  expect(formik.setValues).toHaveBeenCalledWith(
78
- jasmine.objectContaining({
79
+ expect.objectContaining({
79
80
  loadBalancingPolicy: {
80
81
  balancingMode: 'UTILIZATION',
81
82
  capacityScaler: 1,
@@ -89,7 +90,7 @@ describe('GCE server group Load Balancers page', () => {
89
90
  it('refreshes backing data while preserving selected load balancers', async () => {
90
91
  const values = command({ loadBalancers: ['persisted-lb'] });
91
92
  const { adapter, formik } = testProps(values);
92
- adapter.applyConfigurationRefresh.and.resolveTo({
93
+ adapter.applyConfigurationRefresh.mockResolvedValue({
93
94
  command: {
94
95
  ...values,
95
96
  backingData: { ...values.backingData, refreshed: true },
@@ -97,15 +98,15 @@ describe('GCE server group Load Balancers page', () => {
97
98
  },
98
99
  result: { dirty: {} },
99
100
  });
100
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
101
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
101
102
 
102
- wrapper.find('button[aria-label="Refresh load balancers"]').simulate('click');
103
+ fireEvent.click(wrapper.getByRole('button', { name: 'Refresh load balancers' }));
103
104
  await flush();
104
105
 
105
106
  expect(adapter.applyConfigurationRefresh).toHaveBeenCalledWith(values, 'refreshLoadBalancers');
106
107
  expect(formik.setValues).toHaveBeenCalledWith(
107
- jasmine.objectContaining({
108
- backingData: jasmine.objectContaining({ refreshed: true }),
108
+ expect.objectContaining({
109
+ backingData: expect.objectContaining({ refreshed: true }),
109
110
  loadBalancers: ['persisted-lb'],
110
111
  }),
111
112
  );
@@ -126,7 +127,7 @@ describe('GCE server group Load Balancers page', () => {
126
127
  },
127
128
  });
128
129
  const { formik } = testProps(values);
129
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
130
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} />);
130
131
 
131
132
  expect(selectOptions(wrapper, 'Backend services for regional-lb')).toEqual([
132
133
  ['backend-a', 'backend-a'],
@@ -146,8 +147,8 @@ describe('GCE server group Load Balancers page', () => {
146
147
  ['RATE', 'RATE'],
147
148
  ['UTILIZATION', 'UTILIZATION'],
148
149
  ]);
149
- expect(wrapper.find('input[aria-label="Capacity scaler"]').prop('value')).toBe(80);
150
- expect(wrapper.find('input[aria-label="Max rate per instance"]').prop('value')).toBe(50);
150
+ expect(wrapper.getByLabelText('Capacity scaler')).toHaveValue(80);
151
+ expect(wrapper.getByLabelText('Max rate per instance')).toHaveValue(50);
151
152
  });
152
153
 
153
154
  it('round-trips load-balancer metadata without dropping unavailable or unrelated references', async () => {
@@ -159,24 +160,22 @@ describe('GCE server group Load Balancers page', () => {
159
160
  },
160
161
  });
161
162
  const { adapter, formik } = testProps(values);
162
- adapter.applyConfigurationUpdate.and.callFake(async (nextCommand) => ({
163
+ adapter.applyConfigurationUpdate.mockImplementation(async (nextCommand) => ({
163
164
  command: { ...nextCommand, loadBalancers: ['global-lb'], loadBalancerMetadata: {} },
164
165
  result: { dirty: { loadBalancers: ['persisted-lb'] } },
165
166
  }));
166
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
167
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
167
168
 
168
- wrapper.find('select[aria-label="Load balancers"]').simulate('change', {
169
- target: { selectedOptions: [{ value: 'global-lb' }, { value: 'persisted-lb' }] },
170
- });
169
+ changeMultiSelect(wrapper.getByLabelText('Load balancers'), ['global-lb', 'persisted-lb']);
171
170
  await flush();
172
171
 
173
- const changedCommand = adapter.applyConfigurationUpdate.calls.mostRecent().args[0];
172
+ const changedCommand = adapter.applyConfigurationUpdate.mock.lastCall[0];
174
173
  expect(changedCommand.loadBalancerMetadata).toEqual({
175
174
  'global-load-balancer-names': ['persisted-listener', 'global-lb'],
176
175
  'load-balancer-names': ['unrelated-regional-listener'],
177
176
  });
178
177
  expect(formik.setValues).toHaveBeenCalledWith(
179
- jasmine.objectContaining({
178
+ expect.objectContaining({
180
179
  loadBalancers: ['global-lb', 'persisted-lb'],
181
180
  loadBalancerMetadata: {
182
181
  'global-load-balancer-names': ['persisted-listener', 'global-lb'],
@@ -197,7 +196,7 @@ describe('GCE server group Load Balancers page', () => {
197
196
  backendServiceMetadata: ['old-backend', 'persisted-backend', 'other-backend'],
198
197
  });
199
198
  const { adapter, formik } = testProps(values);
200
- adapter.applyConfigurationUpdate.and.callFake(async (nextCommand) => ({
199
+ adapter.applyConfigurationUpdate.mockImplementation(async (nextCommand) => ({
201
200
  command: {
202
201
  ...nextCommand,
203
202
  backendServices: { 'global-lb': ['configured-backend'] },
@@ -205,16 +204,14 @@ describe('GCE server group Load Balancers page', () => {
205
204
  },
206
205
  result: { dirty: {} },
207
206
  }));
208
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
207
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
209
208
 
210
- wrapper.find('select[aria-label="Load balancers"]').simulate('change', {
211
- target: { selectedOptions: [{ value: 'global-lb' }, { value: 'persisted-lb' }] },
212
- });
209
+ changeMultiSelect(wrapper.getByLabelText('Load balancers'), ['global-lb', 'persisted-lb']);
213
210
  await flush();
214
211
 
215
- expect(adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancers).toEqual(['global-lb']);
212
+ expect(adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancers).toEqual(['global-lb']);
216
213
  expect(formik.setValues).toHaveBeenCalledWith(
217
- jasmine.objectContaining({
214
+ expect.objectContaining({
218
215
  loadBalancers: ['global-lb', 'persisted-lb'],
219
216
  backendServices: {
220
217
  'persisted-lb': ['persisted-backend'],
@@ -236,7 +233,7 @@ describe('GCE server group Load Balancers page', () => {
236
233
  backendServiceMetadata: ['backend-a', 'persisted-backend', 'other-backend'],
237
234
  });
238
235
  const { adapter, formik } = testProps(values);
239
- adapter.applyConfigurationUpdate.and.callFake(async (nextCommand) => ({
236
+ adapter.applyConfigurationUpdate.mockImplementation(async (nextCommand) => ({
240
237
  command: {
241
238
  ...nextCommand,
242
239
  backendServices: { 'regional-lb': ['backend-b'] },
@@ -244,21 +241,19 @@ describe('GCE server group Load Balancers page', () => {
244
241
  },
245
242
  result: { dirty: {} },
246
243
  }));
247
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
244
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
248
245
 
249
- wrapper.find('select[aria-label="Backend services for regional-lb"]').simulate('change', {
250
- target: { selectedOptions: [{ value: 'backend-b' }, { value: 'persisted-backend' }] },
251
- });
246
+ changeMultiSelect(wrapper.getByLabelText('Backend services for regional-lb'), ['backend-b', 'persisted-backend']);
252
247
  await flush();
253
248
 
254
- const changedCommand = adapter.applyConfigurationUpdate.calls.mostRecent().args[0];
249
+ const changedCommand = adapter.applyConfigurationUpdate.mock.lastCall[0];
255
250
  expect(changedCommand.backendServices).toEqual({
256
251
  'regional-lb': ['backend-b', 'persisted-backend'],
257
252
  'other-lb': ['other-backend'],
258
253
  });
259
254
  expect(changedCommand.backendServiceMetadata).toEqual(['backend-b', 'persisted-backend', 'other-backend']);
260
255
  expect(formik.setValues).toHaveBeenCalledWith(
261
- jasmine.objectContaining({
256
+ expect.objectContaining({
262
257
  backendServices: {
263
258
  'regional-lb': ['backend-b', 'persisted-backend'],
264
259
  'other-lb': ['other-backend'],
@@ -281,12 +276,12 @@ describe('GCE server group Load Balancers page', () => {
281
276
  },
282
277
  });
283
278
  const { adapter, formik } = testProps(values);
284
- const wrapper = shallow(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
279
+ const wrapper = renderPage(<GceServerGroupLoadBalancers app={{} as any} formik={formik} adapter={adapter} />);
285
280
 
286
- wrapper.find('select[aria-label="Balancing mode"]').simulate('change', { target: { value: 'UTILIZATION' } });
281
+ fireEvent.change(wrapper.getByLabelText('Balancing mode'), { target: { value: 'UTILIZATION' } });
287
282
  await flush();
288
283
 
289
- const changedCommand = adapter.applyConfigurationUpdate.calls.mostRecent().args[0];
284
+ const changedCommand = adapter.applyConfigurationUpdate.mock.lastCall[0];
290
285
  expect(changedCommand.loadBalancingPolicy).toEqual({
291
286
  balancingMode: 'UTILIZATION',
292
287
  capacityScaler: 1,
@@ -294,7 +289,7 @@ describe('GCE server group Load Balancers page', () => {
294
289
  unknownField: 'keep',
295
290
  });
296
291
  expect(adapter.applyConfigurationUpdate).toHaveBeenCalledWith(changedCommand, 'configureLoadBalancerOptions');
297
- expect(formik.setValues).toHaveBeenCalledWith(jasmine.objectContaining(changedCommand));
292
+ expect(formik.setValues).toHaveBeenCalledWith(expect.objectContaining(changedCommand));
298
293
  });
299
294
 
300
295
  it('derives balancing modes from selected load-balancer types and renders the active limit', () => {
@@ -307,14 +302,14 @@ describe('GCE server group Load Balancers page', () => {
307
302
  namedPorts: [],
308
303
  },
309
304
  });
310
- const connectionWrapper = shallow(
305
+ const connectionWrapper = renderPage(
311
306
  <GceServerGroupLoadBalancers app={{} as any} formik={testProps(connectionValues).formik} />,
312
307
  );
313
308
  expect(selectOptions(connectionWrapper, 'Balancing mode')).toEqual([
314
309
  ['CONNECTION', 'CONNECTION'],
315
310
  ['UTILIZATION', 'UTILIZATION'],
316
311
  ]);
317
- expect(connectionWrapper.find('input[aria-label="Max connections per instance"]').prop('value')).toBe(25);
312
+ expect(connectionWrapper.getByLabelText('Max connections per instance')).toHaveValue(25);
318
313
 
319
314
  const mixedValues = command({
320
315
  loadBalancers: ['regional-lb', 'global-lb'],
@@ -325,11 +320,11 @@ describe('GCE server group Load Balancers page', () => {
325
320
  namedPorts: [],
326
321
  },
327
322
  });
328
- const mixedWrapper = shallow(
323
+ const mixedWrapper = renderPage(
329
324
  <GceServerGroupLoadBalancers app={{} as any} formik={testProps(mixedValues).formik} />,
330
325
  );
331
326
  expect(selectOptions(mixedWrapper, 'Balancing mode')).toEqual([['UTILIZATION', 'UTILIZATION']]);
332
- expect(mixedWrapper.find('input[aria-label="Max utilization"]').prop('value')).toBe(70);
327
+ expect(mixedWrapper.getByLabelText('Max utilization')).toHaveValue(70);
333
328
  });
334
329
 
335
330
  it('round-trips named-port and percentage edits and supports adding and removing mappings', async () => {
@@ -343,51 +338,47 @@ describe('GCE server group Load Balancers page', () => {
343
338
  },
344
339
  });
345
340
  const nameProps = testProps(values);
346
- const nameWrapper = shallow(
341
+ const nameWrapper = renderPage(
347
342
  <GceServerGroupLoadBalancers app={{} as any} formik={nameProps.formik} adapter={nameProps.adapter} />,
348
343
  );
349
- nameWrapper.find('select[aria-label="Named port name 1"]').simulate('change', { target: { value: 'metrics' } });
344
+ fireEvent.change(nameWrapper.getByLabelText('Named port name 1'), { target: { value: 'metrics' } });
350
345
  await flush();
351
- expect(
352
- nameProps.adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancingPolicy.namedPorts,
353
- ).toEqual([{ name: 'metrics', port: 8080 }]);
346
+ expect(nameProps.adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancingPolicy.namedPorts).toEqual([
347
+ { name: 'metrics', port: 8080 },
348
+ ]);
354
349
 
355
350
  const capacityProps = testProps(values);
356
- const capacityWrapper = shallow(
351
+ const capacityWrapper = renderPage(
357
352
  <GceServerGroupLoadBalancers app={{} as any} formik={capacityProps.formik} adapter={capacityProps.adapter} />,
358
353
  );
359
- capacityWrapper.find('input[aria-label="Capacity scaler"]').simulate('change', { target: { value: '75' } });
354
+ fireEvent.change(capacityWrapper.getByLabelText('Capacity scaler'), { target: { value: '75' } });
360
355
  await flush();
361
- expect(
362
- capacityProps.adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancingPolicy.capacityScaler,
363
- ).toBe(0.75);
356
+ expect(capacityProps.adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancingPolicy.capacityScaler).toBe(
357
+ 0.75,
358
+ );
364
359
 
365
360
  const addProps = testProps(values);
366
- const addWrapper = shallow(
361
+ const addWrapper = renderPage(
367
362
  <GceServerGroupLoadBalancers app={{} as any} formik={addProps.formik} adapter={addProps.adapter} />,
368
363
  );
369
- addWrapper.find('button[aria-label="Add named port"]').simulate('click');
364
+ fireEvent.click(addWrapper.getByRole('button', { name: 'Add named port' }));
370
365
  await flush();
371
- expect(addProps.adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancingPolicy.namedPorts).toEqual(
372
- [
373
- { name: 'http', port: 8080 },
374
- { name: '', port: 80 },
375
- ],
376
- );
366
+ expect(addProps.adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancingPolicy.namedPorts).toEqual([
367
+ { name: 'http', port: 8080 },
368
+ { name: '', port: 80 },
369
+ ]);
377
370
 
378
371
  const removeProps = testProps(values);
379
- const removeWrapper = shallow(
372
+ const removeWrapper = renderPage(
380
373
  <GceServerGroupLoadBalancers app={{} as any} formik={removeProps.formik} adapter={removeProps.adapter} />,
381
374
  );
382
- removeWrapper.find('button[aria-label="Remove named port 1"]').simulate('click');
375
+ fireEvent.click(removeWrapper.getByRole('button', { name: 'Remove named port 1' }));
383
376
  await flush();
384
- expect(
385
- removeProps.adapter.applyConfigurationUpdate.calls.mostRecent().args[0].loadBalancingPolicy.namedPorts,
386
- ).toEqual([]);
377
+ expect(removeProps.adapter.applyConfigurationUpdate.mock.lastCall[0].loadBalancingPolicy.namedPorts).toEqual([]);
387
378
  });
388
379
 
389
380
  it('renders policy validation errors and associates them with the invalid controls', () => {
390
- const rateWrapper = shallow(
381
+ const rateWrapper = renderPage(
391
382
  <GceServerGroupLoadBalancers
392
383
  app={{} as any}
393
384
  formik={
@@ -426,7 +417,7 @@ describe('GCE server group Load Balancers page', () => {
426
417
  'Max rate must be',
427
418
  );
428
419
 
429
- const modeWrapper = shallow(
420
+ const modeWrapper = renderPage(
430
421
  <GceServerGroupLoadBalancers
431
422
  app={{} as any}
432
423
  formik={
@@ -441,7 +432,7 @@ describe('GCE server group Load Balancers page', () => {
441
432
  );
442
433
  expectPolicyError(modeWrapper, 'select[aria-label="Balancing mode"]', 'balancing-mode', 'Select a balancing mode');
443
434
 
444
- const connectionWrapper = shallow(
435
+ const connectionWrapper = renderPage(
445
436
  <GceServerGroupLoadBalancers
446
437
  app={{} as any}
447
438
  formik={
@@ -466,7 +457,7 @@ describe('GCE server group Load Balancers page', () => {
466
457
  'Max connections must be',
467
458
  );
468
459
 
469
- const utilizationWrapper = shallow(
460
+ const utilizationWrapper = renderPage(
470
461
  <GceServerGroupLoadBalancers
471
462
  app={{} as any}
472
463
  formik={
@@ -503,11 +494,7 @@ describe('GCE server group Load Balancers page', () => {
503
494
  },
504
495
  });
505
496
  const { formik } = testProps(invalid);
506
- const page = shallow(
507
- <GceServerGroupLoadBalancers app={{} as any} formik={formik} />,
508
- ).instance() as GceServerGroupLoadBalancers;
509
-
510
- expect(page.validate(invalid)).toEqual({
497
+ expect(validateGceServerGroupLoadBalancers(invalid)).toEqual({
511
498
  loadBalancingPolicy: {
512
499
  capacityScaler: 'Capacity must be between 0 and 100%.',
513
500
  maxRatePerInstance: 'Max rate must be a finite number greater than or equal to zero.',
@@ -525,8 +512,8 @@ describe('GCE server group Load Balancers page', () => {
525
512
  namedPorts: [{ name: 'http', port: '${ parameters.port }' }],
526
513
  },
527
514
  });
528
- expect(page.validate(pipeline)).toEqual({});
529
- expect(page.validate({ ...pipeline, viewState: { mode: 'create', dirty: {} } })).toEqual({
515
+ expect(validateGceServerGroupLoadBalancers(pipeline)).toEqual({});
516
+ expect(validateGceServerGroupLoadBalancers({ ...pipeline, viewState: { mode: 'create', dirty: {} } })).toEqual({
530
517
  loadBalancingPolicy: {
531
518
  capacityScaler: 'Capacity must be between 0 and 100%.',
532
519
  maxRatePerInstance: 'Max rate must be a finite number greater than or equal to zero.',
@@ -543,47 +530,181 @@ describe('GCE server group Load Balancers page', () => {
543
530
  namedPorts: [],
544
531
  },
545
532
  });
546
- expect(page.validate(blank)).toEqual({
533
+ expect(validateGceServerGroupLoadBalancers(blank)).toEqual({
547
534
  loadBalancingPolicy: {
548
535
  capacityScaler: 'Capacity must be between 0 and 100%.',
549
536
  maxRatePerInstance: 'Max rate must be a finite number greater than or equal to zero.',
550
537
  },
551
538
  });
552
539
  });
540
+
541
+ it('rejects ambiguous cross-scope names and mixed passthrough attachments', () => {
542
+ const ambiguous = command({
543
+ loadBalancers: ['shared-name'],
544
+ backingData: {
545
+ ...command().backingData,
546
+ loadBalancers: [
547
+ {
548
+ accounts: [
549
+ {
550
+ name: 'account-a',
551
+ regions: [
552
+ {
553
+ loadBalancers: [
554
+ { loadBalancerType: 'TCP', name: 'shared-name', region: 'global' },
555
+ {
556
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
557
+ name: 'shared-name',
558
+ region: 'us-central1',
559
+ },
560
+ ],
561
+ },
562
+ ],
563
+ },
564
+ ],
565
+ },
566
+ ],
567
+ },
568
+ });
569
+ const ambiguousMessage =
570
+ 'The selected load balancer name is ambiguous across regions or load balancer types. Rename it or select an unambiguous load balancer.';
571
+ expect(validateGceServerGroupLoadBalancers(ambiguous).loadBalancers).toBe(ambiguousMessage);
572
+ const ambiguousWrapper = renderPage(
573
+ <GceServerGroupLoadBalancers app={{} as any} formik={testProps(ambiguous).formik} />,
574
+ );
575
+ expect(ambiguousWrapper.getByRole('alert')).toHaveTextContent(ambiguousMessage);
576
+ ambiguousWrapper.unmount();
577
+
578
+ const mixedPassthrough = command({
579
+ loadBalancers: ['regional-network', 'regional-lb'],
580
+ backingData: {
581
+ ...command().backingData,
582
+ filtered: {
583
+ loadBalancerIndex: {
584
+ 'regional-lb': command().backingData.filtered.loadBalancerIndex['regional-lb'],
585
+ 'regional-network': {
586
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
587
+ name: 'regional-network',
588
+ },
589
+ },
590
+ },
591
+ },
592
+ });
593
+ const mixedMessage =
594
+ 'REGIONAL_EXTERNAL_NETWORK load balancers cannot be combined with other load balancer families in this editor.';
595
+ expect(validateGceServerGroupLoadBalancers(mixedPassthrough).loadBalancers).toBe(mixedMessage);
596
+ const mixedWrapper = renderPage(
597
+ <GceServerGroupLoadBalancers app={{} as any} formik={testProps(mixedPassthrough).formik} />,
598
+ );
599
+ expect(
600
+ mixedWrapper.container.querySelector('#gce-server-group-load-balancers-error[role="alert"]'),
601
+ ).toHaveTextContent(mixedMessage);
602
+ });
603
+
604
+ it('treats EXTERNAL_MANAGED as HTTP and REGIONAL_EXTERNAL_NETWORK as policy-free', () => {
605
+ const externalManaged = command({
606
+ loadBalancers: ['external-managed'],
607
+ loadBalancingPolicy: {
608
+ balancingMode: 'RATE',
609
+ capacityScaler: 1,
610
+ maxRatePerInstance: 50,
611
+ namedPorts: [{ name: 'http', port: 80 }],
612
+ },
613
+ backingData: {
614
+ ...command().backingData,
615
+ filtered: {
616
+ loadBalancerIndex: {
617
+ 'external-managed': {
618
+ backendServices: [{ name: 'backend-a', portName: 'http' }],
619
+ listeners: [{ name: 'frontend' }],
620
+ loadBalancerType: 'EXTERNAL_MANAGED',
621
+ name: 'external-managed',
622
+ },
623
+ },
624
+ },
625
+ },
626
+ });
627
+ const externalWrapper = renderPage(
628
+ <GceServerGroupLoadBalancers app={{} as any} formik={testProps(externalManaged).formik} />,
629
+ );
630
+ expect(selectOptions(externalWrapper, 'Balancing mode')).toEqual([
631
+ ['RATE', 'RATE'],
632
+ ['UTILIZATION', 'UTILIZATION'],
633
+ ]);
634
+ externalWrapper.unmount();
635
+
636
+ const passthrough = command({
637
+ loadBalancers: ['regional-network'],
638
+ loadBalancingPolicy: {
639
+ balancingMode: 'UTILIZATION',
640
+ capacityScaler: 1,
641
+ maxUtilization: 0.8,
642
+ namedPorts: [{ name: 'http', port: 80 }],
643
+ },
644
+ backingData: {
645
+ ...command().backingData,
646
+ filtered: {
647
+ loadBalancerIndex: {
648
+ 'regional-network': {
649
+ backendServices: ['backend-a'],
650
+ loadBalancerType: 'REGIONAL_EXTERNAL_NETWORK',
651
+ name: 'regional-network',
652
+ },
653
+ },
654
+ },
655
+ },
656
+ });
657
+ const passthroughWrapper = renderPage(
658
+ <GceServerGroupLoadBalancers app={{} as any} formik={testProps(passthrough).formik} />,
659
+ );
660
+ expect(passthroughWrapper.queryByLabelText('Balancing mode')).not.toBeInTheDocument();
661
+ expect(passthroughWrapper.queryByLabelText('Backend services for regional-network')).not.toBeInTheDocument();
662
+ expect(passthroughWrapper.queryByRole('alert')).not.toBeInTheDocument();
663
+ expect(validateGceServerGroupLoadBalancers(passthrough)).toEqual({});
664
+ });
553
665
  });
554
666
 
555
- function selectOptions(wrapper: ReturnType<typeof shallow>, ariaLabel = 'Load balancers'): string[][] {
556
- return wrapper
557
- .find(`select[aria-label="${ariaLabel}"] option`)
558
- .map((option) => [option.prop('value') as string, option.text()]);
667
+ function selectOptions(wrapper: RenderResult, ariaLabel = 'Load balancers'): string[][] {
668
+ return Array.from(wrapper.getByLabelText(ariaLabel).querySelectorAll('option')).map((option) => [
669
+ option.value,
670
+ option.textContent || '',
671
+ ]);
559
672
  }
560
673
 
561
- function expectPolicyError(
562
- wrapper: ReturnType<typeof shallow>,
563
- selector: string,
564
- errorName: string,
565
- message: string,
566
- ): void {
674
+ function expectPolicyError(wrapper: RenderResult, selector: string, errorName: string, message: string): void {
567
675
  const id = `gce-load-balancing-policy-${errorName}-error`;
568
- expect(wrapper.find(selector).prop('aria-invalid')).toBe(true);
569
- expect(wrapper.find(selector).prop('aria-describedby')).toBe(id);
570
- expect(wrapper.find(`#${id}[role="alert"]`).text()).toContain(message);
676
+ expect(wrapper.container.querySelector(selector)).toHaveAttribute('aria-invalid', 'true');
677
+ expect(wrapper.container.querySelector(selector)).toHaveAttribute('aria-describedby', id);
678
+ expect(wrapper.container.querySelector(`#${id}[role="alert"]`)).toHaveTextContent(message);
679
+ }
680
+
681
+ function selectedValues(select: HTMLElement): string[] {
682
+ return Array.from((select as HTMLSelectElement).selectedOptions).map((option) => option.value);
683
+ }
684
+
685
+ function changeMultiSelect(select: HTMLElement, values: string[]): void {
686
+ Array.from((select as HTMLSelectElement).options).forEach((option) => {
687
+ option.selected = values.includes(option.value);
688
+ });
689
+ fireEvent.change(select);
571
690
  }
572
691
 
573
692
  function testProps(values = command()) {
574
693
  const formik = ({
575
694
  values,
576
- setFieldValue: jasmine.createSpy('setFieldValue'),
577
- setValues: jasmine.createSpy('setValues'),
695
+ setFieldValue: vi.fn(),
696
+ setValues: vi.fn(),
578
697
  } as unknown) as FormikProps<IGceServerGroupCommand>;
579
698
  const adapter = ({
580
- applyConfigurationRefresh: jasmine
581
- .createSpy('applyConfigurationRefresh')
582
- .and.callFake(async (nextCommand: IGceServerGroupCommand) => ({ command: nextCommand, result: { dirty: {} } })),
583
- applyConfigurationUpdate: jasmine
584
- .createSpy('applyConfigurationUpdate')
585
- .and.callFake(async (nextCommand: IGceServerGroupCommand) => ({ command: nextCommand, result: { dirty: {} } })),
586
- } as unknown) as jasmine.SpyObj<IGceServerGroupWizardAdapter>;
699
+ applyConfigurationRefresh: vi.fn().mockImplementation(async (nextCommand: IGceServerGroupCommand) => ({
700
+ command: nextCommand,
701
+ result: { dirty: {} },
702
+ })),
703
+ applyConfigurationUpdate: vi.fn().mockImplementation(async (nextCommand: IGceServerGroupCommand) => ({
704
+ command: nextCommand,
705
+ result: { dirty: {} },
706
+ })),
707
+ } as unknown) as Mocked<IGceServerGroupWizardAdapter>;
587
708
  return { adapter, formik };
588
709
  }
589
710
 
@@ -641,3 +762,8 @@ function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGro
641
762
  function flush(): Promise<void> {
642
763
  return new Promise((resolve) => setTimeout(resolve));
643
764
  }
765
+
766
+ function renderPage(component: React.ReactElement): RenderResult {
767
+ const container = document.body.appendChild(document.createElement('div'));
768
+ return render(component, { baseElement: container, container });
769
+ }