@spinnaker/google 2026.3.2 → 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
@@ -25,49 +25,53 @@ type CapacityValue = number | string | null | undefined;
25
25
 
26
26
  const INTEGER_ERROR = 'must be a finite non-negative integer.';
27
27
 
28
- export class ServerGroupCapacity extends GceServerGroupWizardPage {
29
- public validate(values: IGceServerGroupCommand): ICapacityValidationErrors {
30
- const errors: ICapacityValidationErrors = {};
31
- const desired = values.capacity?.desired;
28
+ export function validateGceServerGroupCapacity(values: IGceServerGroupCommand): ICapacityValidationErrors {
29
+ const errors: ICapacityValidationErrors = {};
30
+ const desired = values.capacity?.desired;
32
31
 
33
- if (!isValidCapacityValue(desired, values)) {
34
- errors.capacity = { desired: `Desired capacity ${INTEGER_ERROR}` };
35
- }
36
-
37
- if (values.autoscalingPolicy) {
38
- const min = values.autoscalingPolicy?.minNumReplicas;
39
- const max = values.autoscalingPolicy?.maxNumReplicas;
32
+ if (!isValidCapacityValue(desired, values)) {
33
+ errors.capacity = { desired: `Desired capacity ${INTEGER_ERROR}` };
34
+ }
40
35
 
41
- if (!isValidCapacityValue(min, values)) {
42
- errors.autoscalingPolicy = {
43
- ...errors.autoscalingPolicy,
44
- minNumReplicas: `Minimum capacity ${INTEGER_ERROR}`,
45
- };
46
- }
47
- if (!isValidCapacityValue(max, values)) {
48
- errors.autoscalingPolicy = {
49
- ...errors.autoscalingPolicy,
50
- maxNumReplicas: `Maximum capacity ${INTEGER_ERROR}`,
51
- };
52
- }
53
- if (isNonNegativeInteger(desired) && isNonNegativeInteger(min) && desired < min) {
54
- errors.capacity = { desired: 'Desired capacity must be at least minimum capacity.' };
55
- } else if (isNonNegativeInteger(desired) && isNonNegativeInteger(max) && desired > max) {
56
- errors.capacity = { desired: 'Desired capacity must not exceed maximum capacity.' };
57
- }
58
- }
36
+ if (values.autoscalingPolicy) {
37
+ const min = values.autoscalingPolicy?.minNumReplicas;
38
+ const max = values.autoscalingPolicy?.maxNumReplicas;
59
39
 
60
- if (!values.regional && !values.zone?.trim()) {
61
- errors.zone = 'Zone required.';
40
+ if (!isValidCapacityValue(min, values)) {
41
+ errors.autoscalingPolicy = {
42
+ ...errors.autoscalingPolicy,
43
+ minNumReplicas: `Minimum capacity ${INTEGER_ERROR}`,
44
+ };
62
45
  }
63
- if (values.regional && values.selectZones && !values.distributionPolicy?.zones?.length) {
64
- errors.distributionPolicy = { zones: 'At least one zone required.' };
46
+ if (!isValidCapacityValue(max, values)) {
47
+ errors.autoscalingPolicy = {
48
+ ...errors.autoscalingPolicy,
49
+ maxNumReplicas: `Maximum capacity ${INTEGER_ERROR}`,
50
+ };
65
51
  }
66
- if (!hasValidFlexibilityPolicy(values)) {
67
- errors.instanceFlexibilityPolicy = 'Instance flexibility policy is invalid.';
52
+ if (isNonNegativeInteger(desired) && isNonNegativeInteger(min) && desired < min) {
53
+ errors.capacity = { desired: 'Desired capacity must be at least minimum capacity.' };
54
+ } else if (isNonNegativeInteger(desired) && isNonNegativeInteger(max) && desired > max) {
55
+ errors.capacity = { desired: 'Desired capacity must not exceed maximum capacity.' };
68
56
  }
57
+ }
69
58
 
70
- return errors;
59
+ if (!values.regional && !values.zone?.trim()) {
60
+ errors.zone = 'Zone required.';
61
+ }
62
+ if (values.regional && values.selectZones && !values.distributionPolicy?.zones?.length) {
63
+ errors.distributionPolicy = { zones: 'At least one zone required.' };
64
+ }
65
+ if (!hasValidFlexibilityPolicy(values)) {
66
+ errors.instanceFlexibilityPolicy = 'Instance flexibility policy is invalid.';
67
+ }
68
+
69
+ return errors;
70
+ }
71
+
72
+ export class ServerGroupCapacity extends GceServerGroupWizardPage {
73
+ public validate(values: IGceServerGroupCommand): ICapacityValidationErrors {
74
+ return validateGceServerGroupCapacity(values);
71
75
  }
72
76
 
73
77
  private commandChanged = (
@@ -87,7 +91,6 @@ export class ServerGroupCapacity extends GceServerGroupWizardPage {
87
91
  if (value === undefined) {
88
92
  return;
89
93
  }
90
-
91
94
  const { values } = this.props.formik;
92
95
  if (!values.autoscalingPolicy) {
93
96
  this.props.formik.setFieldValue('capacity', { min: value, max: value, desired: value });
@@ -1,41 +1,53 @@
1
- import type { FormikProps } from 'formik';
1
+ import { fireEvent, render, screen, within } from '@testing-library/react';
2
2
  import React from 'react';
3
- import { shallow } from 'enzyme';
4
3
 
5
- import { Markdown, StageArtifactSelectorDelegate } from '@spinnaker/core';
6
-
7
- import { ServerGroupImageSettings } from './ServerGroupImageSettings';
8
4
  import type { IGceServerGroupCommand } from '../GceServerGroupWizard.types';
5
+ import { ServerGroupImageSettings, validateGceServerGroupImageSettings } from './ServerGroupImageSettings';
6
+
7
+ vi.mock('@spinnaker/core', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('@spinnaker/core')>();
9
+ return {
10
+ ...actual,
11
+ StageArtifactSelectorDelegate: ({ onArtifactEdited, onExpectedArtifactSelected }: any) => (
12
+ <div data-testid="artifact-selector">
13
+ <button onClick={() => onArtifactEdited({ type: 'custom/object', reference: 'new' })} type="button">
14
+ Edit artifact
15
+ </button>
16
+ <button onClick={() => onExpectedArtifactSelected({ id: 'expected-id' })} type="button">
17
+ Select expected artifact
18
+ </button>
19
+ </div>
20
+ ),
21
+ };
22
+ });
9
23
 
10
24
  describe('ServerGroupImageSettings', () => {
11
25
  it('renders accessible image and source controls while preserving unavailable persisted values', () => {
12
- const formik = testFormik(
13
- command({ backingData: { allImages: [{ imageName: 'known-image' }, { imageName: 'known-image' }] } }),
14
- );
15
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={formik} />);
26
+ const values = command({
27
+ backingData: { allImages: [{ imageName: 'known-image' }, { imageName: 'known-image' }] },
28
+ });
29
+ const { setFieldValue } = renderImageSettings(values);
16
30
 
17
- expect(selectOptions(wrapper, 'Image source')).toEqual([
31
+ expect(selectOptions('Image source')).toEqual([
18
32
  ['artifact', 'Artifact'],
19
33
  ['priorStage', 'Prior Stage'],
20
34
  ['persisted-source', 'persisted-source (unavailable)'],
21
35
  ]);
22
- expect(selectOptions(wrapper, 'Image')).toEqual([
36
+ expect(selectOptions('Image')).toEqual([
23
37
  ['', 'Select...'],
24
38
  ['known-image', 'known-image'],
25
39
  ['persisted-image', 'persisted-image (unavailable)'],
26
40
  ]);
27
- expect(formik.setFieldValue).not.toHaveBeenCalled();
28
- expect(formik.setValues).not.toHaveBeenCalled();
41
+ expect(setFieldValue).not.toHaveBeenCalled();
29
42
  });
30
43
 
31
44
  it('updates image source and image selection as page-owned fields', () => {
32
- const formik = testFormik();
33
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={formik} />);
45
+ const { setFieldValue } = renderImageSettings(command());
34
46
 
35
- wrapper.find('[aria-label="Image source"]').simulate('change', { target: { value: 'priorStage' } });
36
- wrapper.find('[aria-label="Image"]').simulate('change', { target: { value: 'known-image' } });
47
+ fireEvent.change(screen.getByLabelText('Image source'), { target: { value: 'priorStage' } });
48
+ fireEvent.change(screen.getByLabelText('Image'), { target: { value: 'known-image' } });
37
49
 
38
- expect(formik.setFieldValue.calls.allArgs()).toEqual([
50
+ expect(setFieldValue.mock.calls).toEqual([
39
51
  ['imageSource', 'priorStage'],
40
52
  ['image', 'known-image'],
41
53
  ]);
@@ -46,11 +58,11 @@ describe('ServerGroupImageSettings', () => {
46
58
  imageSource: 'artifact',
47
59
  viewState: { mode: 'editPipeline', showImageSourceSelector: true, imageSourceText: 'From **trigger**' },
48
60
  });
49
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={testFormik(values)} />);
61
+ renderImageSettings(values);
50
62
 
51
- expect(wrapper.find('[aria-label="Image source"]').exists()).toBe(false);
52
- expect(wrapper.find(Markdown).prop('message')).toBe('From **trigger**');
53
- expect(wrapper.find(StageArtifactSelectorDelegate).exists()).toBe(true);
63
+ expect(screen.queryByLabelText('Image source')).not.toBeInTheDocument();
64
+ expect(screen.getByText((_, element) => element?.textContent === 'From trigger')).toBeInTheDocument();
65
+ expect(screen.getByTestId('artifact-selector')).toBeInTheDocument();
54
66
  });
55
67
 
56
68
  it('edits inline and expected image artifacts without retaining the other reference', () => {
@@ -59,17 +71,14 @@ describe('ServerGroupImageSettings', () => {
59
71
  imageArtifactId: 'old-id',
60
72
  imageArtifact: { type: 'custom/object', reference: 'old' },
61
73
  });
62
- const formik = testFormik(values);
63
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={formik} />);
64
- const selector = wrapper.find(StageArtifactSelectorDelegate);
65
- const editedArtifact = { type: 'custom/object', reference: 'new' };
74
+ const { setFieldValue } = renderImageSettings(values);
66
75
 
67
- selector.prop('onArtifactEdited')(editedArtifact as any);
68
- selector.prop('onExpectedArtifactSelected')({ id: 'expected-id' } as any);
76
+ fireEvent.click(screen.getByRole('button', { name: 'Edit artifact' }));
77
+ fireEvent.click(screen.getByRole('button', { name: 'Select expected artifact' }));
69
78
 
70
- expect(formik.setFieldValue.calls.allArgs()).toEqual([
79
+ expect(setFieldValue.mock.calls).toEqual([
71
80
  ['imageArtifactId', null],
72
- ['imageArtifact', editedArtifact],
81
+ ['imageArtifact', { type: 'custom/object', reference: 'new' }],
73
82
  ['imageArtifactId', 'expected-id'],
74
83
  ['imageArtifact', null],
75
84
  ]);
@@ -77,48 +86,45 @@ describe('ServerGroupImageSettings', () => {
77
86
 
78
87
  it('does not render or require image selection when it is disabled', () => {
79
88
  const values = command({ image: null, viewState: { mode: 'editPipeline', disableImageSelection: true } });
80
- const formik = testFormik(values);
81
- const page = new ServerGroupImageSettings({ app: { name: 'app' } as any, formik } as any);
82
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={formik} />);
89
+ renderImageSettings(values);
83
90
 
84
- expect(wrapper.find('[aria-label="Image"]').exists()).toBe(false);
85
- expect(wrapper.text()).toContain('Image selection is disabled for this command.');
86
- expect(page.validate(values)).toEqual({});
91
+ expect(screen.queryByLabelText('Image')).not.toBeInTheDocument();
92
+ expect(screen.getByText('Image selection is disabled for this command.')).toBeInTheDocument();
93
+ expect(validateGceServerGroupImageSettings(values)).toEqual({});
87
94
  });
88
95
 
89
96
  it('requires an image when selection is enabled', () => {
90
- const values = command({ image: '' });
91
- const formik = testFormik(values);
92
- const page = new ServerGroupImageSettings({ app: { name: 'app' } as any, formik } as any);
93
-
94
- expect(page.validate(values)).toEqual({ image: 'Image required.' });
97
+ expect(validateGceServerGroupImageSettings(command({ image: '' }))).toEqual({ image: 'Image required.' });
95
98
  });
96
99
 
97
100
  it('renders image validation next to the required control with an accessible description', () => {
98
- const values = command({ image: '' });
99
- const wrapper = shallow(<ServerGroupImageSettings app={{ name: 'app' } as any} formik={testFormik(values)} />);
100
- const image = wrapper.find('select[aria-label="Image"]');
101
-
102
- expect(image.prop('required')).toBe(true);
103
- expect(image.prop('aria-invalid')).toBe(true);
104
- expect(image.prop('aria-describedby')).toBe('gce-server-group-image-error');
105
- expect(wrapper.find('#gce-server-group-image-error').prop('role')).toBe('alert');
106
- expect(wrapper.find('#gce-server-group-image-error').text()).toBe('Image required.');
101
+ renderImageSettings(command({ image: '' }));
102
+ const image = screen.getByLabelText('Image');
103
+ const alert = screen.getByRole('alert');
104
+
105
+ expect(image).toBeRequired();
106
+ expect(image).toHaveAttribute('aria-invalid', 'true');
107
+ expect(image).toHaveAttribute('aria-describedby', 'gce-server-group-image-error');
108
+ expect(alert).toHaveAttribute('id', 'gce-server-group-image-error');
109
+ expect(alert).toHaveTextContent('Image required.');
107
110
  });
108
111
  });
109
112
 
110
- function selectOptions(wrapper: ReturnType<typeof shallow>, label: string): string[][] {
111
- return wrapper
112
- .find(`[aria-label="${label}"] option`)
113
- .map((option) => [option.prop('value') as string, option.text()]);
113
+ function renderImageSettings(values: IGceServerGroupCommand) {
114
+ const setFieldValue = vi.fn();
115
+ render(
116
+ <ServerGroupImageSettings
117
+ app={{ name: 'app' } as any}
118
+ formik={{ errors: {}, setFieldValue, setValues: vi.fn(), values } as any}
119
+ />,
120
+ );
121
+ return { setFieldValue };
114
122
  }
115
123
 
116
- function testFormik(values = command()): FormikProps<IGceServerGroupCommand> {
117
- return ({
118
- values,
119
- setFieldValue: jasmine.createSpy('setFieldValue'),
120
- setValues: jasmine.createSpy('setValues'),
121
- } as unknown) as FormikProps<IGceServerGroupCommand>;
124
+ function selectOptions(label: string): string[][] {
125
+ return within(screen.getByLabelText(label))
126
+ .getAllByRole('option')
127
+ .map((option) => [(option as HTMLOptionElement).value, option.textContent || '']);
122
128
  }
123
129
 
124
130
  function command(overrides: Partial<IGceServerGroupCommand> = {}): IGceServerGroupCommand {
@@ -19,8 +19,7 @@ const IMAGE_SOURCES = [
19
19
 
20
20
  export class ServerGroupImageSettings extends GceServerGroupWizardPage {
21
21
  public validate(values: IGceServerGroupCommand): IGceServerGroupCommandValidationErrors {
22
- const image = validateGceServerGroupCommand(values).image;
23
- return image ? { image } : {};
22
+ return validateGceServerGroupImageSettings(values);
24
23
  }
25
24
 
26
25
  private imageArtifactEdited = (artifact: any): void => {
@@ -146,6 +145,13 @@ export class ServerGroupImageSettings extends GceServerGroupWizardPage {
146
145
  }
147
146
  }
148
147
 
148
+ export function validateGceServerGroupImageSettings(
149
+ values: IGceServerGroupCommand,
150
+ ): IGceServerGroupCommandValidationErrors {
151
+ const image = validateGceServerGroupCommand(values).image;
152
+ return image ? { image } : {};
153
+ }
154
+
149
155
  function imageOptions(rawImages: readonly any[], persistedImage: string | null | undefined): IImageOption[] {
150
156
  const images = Array.from(
151
157
  new Set(rawImages.map((image) => (typeof image === 'string' ? image : image.imageName)).filter(Boolean)),