@spinnaker/docker 2026.2.3 → 2026.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,187 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+
4
+ import {
5
+ AuthenticationService,
6
+ BakeExecutionLabel,
7
+ BakeryReader,
8
+ ExecutionDetailsTasks,
9
+ Registry,
10
+ SETTINGS,
11
+ Spinner,
12
+ } from '@spinnaker/core';
13
+
14
+ import {
15
+ applyDockerBakeStageDefaults,
16
+ DockerBakeExecutionDetails,
17
+ DockerBakeStageConfig,
18
+ DOCKER_BAKE_STAGE_CONFIG,
19
+ } from './dockerBakeStage';
20
+
21
+ describe('Docker bake stage', () => {
22
+ let originalBakeryDetailUrl: string;
23
+
24
+ beforeEach(() => {
25
+ originalBakeryDetailUrl = SETTINGS.bakeryDetailUrl;
26
+ });
27
+
28
+ beforeEach(() => Registry.reinitialize());
29
+ afterEach(() => {
30
+ SETTINGS.bakeryDetailUrl = originalBakeryDetailUrl;
31
+ Registry.reinitialize();
32
+ });
33
+
34
+ it('registers the package-local React stage config', () => {
35
+ Registry.pipeline.registerStage(DOCKER_BAKE_STAGE_CONFIG);
36
+
37
+ const stageConfig = Registry.pipeline.getStageConfig({ type: 'bake', cloudProvider: 'docker' } as any);
38
+
39
+ expect(stageConfig.provides).toBe('bake');
40
+ expect(stageConfig.cloudProvider).toBe('docker');
41
+ expect(stageConfig.component).toBe(DockerBakeStageConfig);
42
+ expect(stageConfig.executionDetailsSections).toEqual([DockerBakeExecutionDetails, ExecutionDetailsTasks]);
43
+ expect(stageConfig.executionLabelComponent).toBe(BakeExecutionLabel);
44
+ });
45
+
46
+ it('applies Docker bake defaults and removes empty string properties', () => {
47
+ const stage: any = {
48
+ package: 'my-package',
49
+ organization: '',
50
+ extendedAttributes: {},
51
+ };
52
+
53
+ const result = applyDockerBakeStageDefaults(stage, {
54
+ user: 'user@example.com',
55
+ baseOsOptions: [{ id: 'ubuntu' }, { id: 'debian' }],
56
+ baseLabelOptions: ['release', 'snapshot'],
57
+ });
58
+
59
+ expect(result).toEqual({
60
+ package: 'my-package',
61
+ extendedAttributes: {},
62
+ region: 'global',
63
+ user: 'user@example.com',
64
+ baseOs: 'ubuntu',
65
+ baseLabel: 'release',
66
+ });
67
+ expect(result).not.toBe(stage);
68
+ });
69
+
70
+ it('keeps existing Docker bake values when defaults are available', () => {
71
+ const stage: any = {
72
+ region: 'custom-region',
73
+ user: 'existing-user',
74
+ baseOs: 'debian',
75
+ baseLabel: 'snapshot',
76
+ };
77
+
78
+ const result = applyDockerBakeStageDefaults(stage, {
79
+ user: 'user@example.com',
80
+ baseOsOptions: [{ id: 'ubuntu' }],
81
+ baseLabelOptions: ['release'],
82
+ });
83
+
84
+ expect(result.region).toBe('custom-region');
85
+ expect(result.user).toBe('existing-user');
86
+ expect(result.baseOs).toBe('debian');
87
+ expect(result.baseLabel).toBe('snapshot');
88
+ });
89
+
90
+ it('persists Docker bake defaults after loading options', async () => {
91
+ spyOn(AuthenticationService, 'getAuthenticatedUser').and.returnValue({ name: 'user@example.com' } as any);
92
+ spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(Promise.resolve({ baseImages: [{ id: 'ubuntu' }] } as any));
93
+ spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(Promise.resolve(['release']));
94
+
95
+ const updateStage = jasmine.createSpy('updateStage');
96
+
97
+ shallow(
98
+ <DockerBakeStageConfig
99
+ application={{} as any}
100
+ pipeline={{} as any}
101
+ stage={{ package: 'my-package', organization: '' } as any}
102
+ stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
103
+ updateStage={updateStage}
104
+ updateStageField={jasmine.createSpy('updateStageField')}
105
+ />,
106
+ );
107
+
108
+ await new Promise((resolve) => setTimeout(resolve, 0));
109
+
110
+ expect(updateStage).toHaveBeenCalledWith({
111
+ package: 'my-package',
112
+ region: 'global',
113
+ user: 'user@example.com',
114
+ baseOs: 'ubuntu',
115
+ baseLabel: 'release',
116
+ });
117
+ });
118
+
119
+ it('shows an error instead of a permanent spinner when bake options fail to load', async () => {
120
+ spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(Promise.reject(new Error('boom')));
121
+ spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(Promise.resolve(['release']));
122
+
123
+ const wrapper = shallow(
124
+ <DockerBakeStageConfig
125
+ application={{} as any}
126
+ pipeline={{} as any}
127
+ stage={{ package: 'my-package' } as any}
128
+ stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
129
+ updateStage={jasmine.createSpy('updateStage')}
130
+ updateStageField={jasmine.createSpy('updateStageField')}
131
+ />,
132
+ );
133
+
134
+ await new Promise((resolve) => setTimeout(resolve, 0));
135
+ wrapper.update();
136
+
137
+ expect(wrapper.find(Spinner).exists()).toBe(false);
138
+ expect(wrapper.text()).toContain('Unable to load Docker bake options');
139
+ });
140
+
141
+ it('does not update state after unmounting before bake options load', async () => {
142
+ let resolveBaseOsOptions: (value: any) => void;
143
+ let resolveBaseLabelOptions: (value: string[]) => void;
144
+ spyOn(BakeryReader, 'getBaseOsOptions').and.returnValue(new Promise((resolve) => (resolveBaseOsOptions = resolve)));
145
+ spyOn(BakeryReader, 'getBaseLabelOptions').and.returnValue(
146
+ new Promise((resolve) => (resolveBaseLabelOptions = resolve)),
147
+ );
148
+
149
+ const wrapper = shallow(
150
+ <DockerBakeStageConfig
151
+ application={{} as any}
152
+ pipeline={{} as any}
153
+ stage={{ package: 'my-package' } as any}
154
+ stageFieldUpdated={jasmine.createSpy('stageFieldUpdated')}
155
+ updateStage={jasmine.createSpy('updateStage')}
156
+ updateStageField={jasmine.createSpy('updateStageField')}
157
+ />,
158
+ );
159
+ const setState = spyOn(wrapper.instance() as DockerBakeStageConfig, 'setState');
160
+
161
+ wrapper.unmount();
162
+ resolveBaseOsOptions!({ baseImages: [{ id: 'ubuntu' }] });
163
+ resolveBaseLabelOptions!(['release']);
164
+ await new Promise((resolve) => setTimeout(resolve, 0));
165
+
166
+ expect(setState).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('replaces every bakery detail URL placeholder occurrence', () => {
170
+ SETTINGS.bakeryDetailUrl =
171
+ '/bakery/{{context.region}}/{{context.region}}/{{context.status.resourceId}}/{{context.status.resourceId}}';
172
+
173
+ const wrapper = shallow(
174
+ <DockerBakeExecutionDetails
175
+ current={true}
176
+ name="bakeConfig"
177
+ stage={
178
+ {
179
+ context: { region: 'us-west-2', status: { resourceId: 'image-123' } },
180
+ } as any
181
+ }
182
+ />,
183
+ );
184
+
185
+ expect(wrapper.find('a').prop('href')).toBe('/bakery/us-west-2/us-west-2/image-123/image-123');
186
+ });
187
+ });
@@ -0,0 +1,313 @@
1
+ import { isEqual } from 'lodash';
2
+ import React from 'react';
3
+
4
+ import type {
5
+ IExecutionDetailsSectionProps,
6
+ IFormikStageConfigInjectedProps,
7
+ IStage,
8
+ IStageConfigProps,
9
+ } from '@spinnaker/core';
10
+ import {
11
+ AuthenticationService,
12
+ BakeExecutionLabel,
13
+ BakeryReader,
14
+ ExecutionDetailsSection,
15
+ ExecutionDetailsTasks,
16
+ FormikStageConfig,
17
+ Registry,
18
+ SETTINGS,
19
+ Spinner,
20
+ StageConfigField,
21
+ StageFailureMessage,
22
+ } from '@spinnaker/core';
23
+
24
+ interface IBaseOsOption {
25
+ id: string;
26
+ shortDescription?: string;
27
+ detailedDescription?: string;
28
+ isImageFamily?: boolean;
29
+ displayName?: string;
30
+ }
31
+
32
+ interface IDockerBakeStageDefaults {
33
+ user: string;
34
+ baseOsOptions: IBaseOsOption[];
35
+ baseLabelOptions: string[];
36
+ }
37
+
38
+ interface IDockerBakeStageConfigState {
39
+ baseLabelOptions: string[];
40
+ baseOsOptions: IBaseOsOption[];
41
+ loadError: boolean;
42
+ loading: boolean;
43
+ }
44
+
45
+ function deleteEmptyProperties(stage: IStage): IStage {
46
+ return Object.keys(stage).reduce((acc, key) => {
47
+ if ((stage as any)[key] !== '') {
48
+ (acc as any)[key] = (stage as any)[key];
49
+ }
50
+ return acc;
51
+ }, {} as IStage);
52
+ }
53
+
54
+ export function applyDockerBakeStageDefaults(stage: IStage, defaults: IDockerBakeStageDefaults): IStage {
55
+ const nextStage = deleteEmptyProperties({ ...stage });
56
+
57
+ nextStage.region = nextStage.region || 'global';
58
+ nextStage.user = nextStage.user || defaults.user;
59
+
60
+ if (!nextStage.baseOs && defaults.baseOsOptions?.length) {
61
+ nextStage.baseOs = defaults.baseOsOptions[0].id;
62
+ }
63
+
64
+ if (!nextStage.baseLabel && defaults.baseLabelOptions?.length) {
65
+ nextStage.baseLabel = defaults.baseLabelOptions[0];
66
+ }
67
+
68
+ return nextStage;
69
+ }
70
+
71
+ function baseOsDescription(baseOsOption: IBaseOsOption): string {
72
+ const baseOsName = baseOsOption?.displayName || baseOsOption?.id || '';
73
+ return baseOsOption?.shortDescription ? `${baseOsName} (${baseOsOption.shortDescription})` : baseOsName;
74
+ }
75
+
76
+ function bakeryDetailUrl(stage: IStage): string {
77
+ const context = stage.context || {};
78
+ const urlTemplate = SETTINGS.bakeryDetailUrl || '';
79
+ return urlTemplate
80
+ .replace(/\{\{context\.region\}\}/g, context.region || '')
81
+ .replace(/\{\{context\.status\.resourceId\}\}/g, context.status?.resourceId || '');
82
+ }
83
+
84
+ function DockerBakeStageForm({
85
+ baseLabelOptions,
86
+ baseOsOptions,
87
+ formik,
88
+ }: IFormikStageConfigInjectedProps & IDockerBakeStageConfigState) {
89
+ const stage = formik.values;
90
+ const extendedAttributes = stage.extendedAttributes || {};
91
+ const setFieldValue = (field: string) => (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
92
+ formik.setFieldValue(field, event.target.value);
93
+ };
94
+
95
+ return (
96
+ <>
97
+ <StageConfigField label="Package" helpKey="pipeline.config.bake.package">
98
+ <input
99
+ type="text"
100
+ className="form-control input-sm"
101
+ value={stage.package || ''}
102
+ onChange={setFieldValue('package')}
103
+ />
104
+ </StageConfigField>
105
+ <StageConfigField label="Organization" helpKey="pipeline.config.docker.bake.organization">
106
+ <input
107
+ type="text"
108
+ className="form-control input-sm"
109
+ value={stage.organization || ''}
110
+ onChange={setFieldValue('organization')}
111
+ />
112
+ </StageConfigField>
113
+ <StageConfigField label="Image Name" helpKey="pipeline.config.docker.bake.targetImage">
114
+ <input
115
+ type="text"
116
+ className="form-control input-sm"
117
+ value={stage.ami_name || ''}
118
+ onChange={setFieldValue('ami_name')}
119
+ />
120
+ </StageConfigField>
121
+ <StageConfigField label="Image tag" helpKey="pipeline.config.docker.bake.targetImageTag">
122
+ <input
123
+ type="text"
124
+ className="form-control input-sm"
125
+ value={extendedAttributes.docker_target_image_tag || ''}
126
+ onChange={setFieldValue('extendedAttributes.docker_target_image_tag')}
127
+ />
128
+ </StageConfigField>
129
+ <StageConfigField label="Base OS">
130
+ <select className="form-control input-sm" value={stage.baseOs || ''} onChange={setFieldValue('baseOs')}>
131
+ {baseOsOptions.map((baseOsOption: IBaseOsOption) => (
132
+ <option key={baseOsOption.id} value={baseOsOption.id}>
133
+ {baseOsDescription(baseOsOption)}
134
+ </option>
135
+ ))}
136
+ </select>
137
+ </StageConfigField>
138
+ <StageConfigField label="Base Label">
139
+ {baseLabelOptions.map((baseLabel: string) => (
140
+ <label key={baseLabel} className="radio-inline">
141
+ <input
142
+ type="radio"
143
+ checked={stage.baseLabel === baseLabel}
144
+ onChange={() => formik.setFieldValue('baseLabel', baseLabel)}
145
+ />
146
+ {baseLabel}
147
+ </label>
148
+ ))}
149
+ </StageConfigField>
150
+ <StageConfigField label="Rebake">
151
+ <div className="checkbox" style={{ marginBottom: 0 }}>
152
+ <label>
153
+ <input
154
+ type="checkbox"
155
+ checked={!!stage.rebake}
156
+ onChange={(event) => formik.setFieldValue('rebake', event.target.checked)}
157
+ />
158
+ Rebake image without regard to the status of any existing bake
159
+ </label>
160
+ </div>
161
+ </StageConfigField>
162
+ </>
163
+ );
164
+ }
165
+
166
+ export class DockerBakeStageConfig extends React.Component<IStageConfigProps, IDockerBakeStageConfigState> {
167
+ private mounted = false;
168
+
169
+ public state: IDockerBakeStageConfigState = {
170
+ baseLabelOptions: [],
171
+ baseOsOptions: [],
172
+ loadError: false,
173
+ loading: true,
174
+ };
175
+
176
+ public componentDidMount(): void {
177
+ this.mounted = true;
178
+ Promise.all([BakeryReader.getBaseOsOptions('docker'), BakeryReader.getBaseLabelOptions()])
179
+ .then(([baseOsOptions, baseLabelOptions]) => {
180
+ if (!this.mounted) {
181
+ return;
182
+ }
183
+
184
+ const baseOsOptionsList = baseOsOptions.baseImages || [];
185
+ const stageWithDefaults = applyDockerBakeStageDefaults(this.props.stage, {
186
+ user: AuthenticationService.getAuthenticatedUser()?.name,
187
+ baseOsOptions: baseOsOptionsList,
188
+ baseLabelOptions,
189
+ });
190
+
191
+ if (!isEqual(stageWithDefaults, this.props.stage)) {
192
+ this.props.updateStage(stageWithDefaults);
193
+ }
194
+
195
+ this.setState({
196
+ baseLabelOptions,
197
+ baseOsOptions: baseOsOptionsList,
198
+ loadError: false,
199
+ loading: false,
200
+ });
201
+ })
202
+ .catch(() => {
203
+ if (!this.mounted) {
204
+ return;
205
+ }
206
+
207
+ this.setState({ loadError: true, loading: false });
208
+ });
209
+ }
210
+
211
+ public componentWillUnmount(): void {
212
+ this.mounted = false;
213
+ }
214
+
215
+ public render() {
216
+ if (this.state.loading) {
217
+ return <Spinner />;
218
+ }
219
+
220
+ if (this.state.loadError) {
221
+ return <div className="alert alert-danger">Unable to load Docker bake options.</div>;
222
+ }
223
+
224
+ const authenticatedUser = AuthenticationService.getAuthenticatedUser();
225
+ const stageWithDefaults = applyDockerBakeStageDefaults(this.props.stage, {
226
+ user: authenticatedUser?.name,
227
+ baseOsOptions: this.state.baseOsOptions,
228
+ baseLabelOptions: this.state.baseLabelOptions,
229
+ });
230
+
231
+ return (
232
+ <FormikStageConfig
233
+ application={this.props.application}
234
+ onChange={this.props.updateStage}
235
+ pipeline={this.props.pipeline}
236
+ stage={stageWithDefaults}
237
+ render={(props: IFormikStageConfigInjectedProps) => <DockerBakeStageForm {...props} {...this.state} />}
238
+ />
239
+ );
240
+ }
241
+ }
242
+
243
+ export function DockerBakeExecutionDetails(props: IExecutionDetailsSectionProps) {
244
+ const { current, name, stage } = props;
245
+ const context = stage.context || {};
246
+ const resourceId = context.status?.resourceId;
247
+
248
+ return (
249
+ <ExecutionDetailsSection name={name} current={current}>
250
+ <div className="row">
251
+ <div className="col-md-6">
252
+ <dl className="dl-narrow dl-horizontal">
253
+ <dt>Organization</dt>
254
+ <dd>{context.organization}</dd>
255
+ <dt>Image Name</dt>
256
+ <dd>{context.ami_name}</dd>
257
+ <dt>Image Tag</dt>
258
+ <dd>{context.extendedAttributes?.docker_target_image_tag}</dd>
259
+ <dt>Image</dt>
260
+ <dd>{context.ami}</dd>
261
+ </dl>
262
+ </div>
263
+ <div className="col-md-6">
264
+ <dl className="dl-narrow dl-horizontal">
265
+ <dt>Base OS</dt>
266
+ <dd>{context.baseOs}</dd>
267
+ <dt>Region</dt>
268
+ <dd>{context.region}</dd>
269
+ <dt>Package</dt>
270
+ <dd>{context.package}</dd>
271
+ <dt>Label</dt>
272
+ <dd>{context.baseLabel}</dd>
273
+ </dl>
274
+ </div>
275
+ </div>
276
+ <StageFailureMessage stage={stage} message={stage.failureMessage} />
277
+ {context.region && resourceId && (
278
+ <div className="row">
279
+ <div className="col-md-12">
280
+ <div className={`alert alert-${stage.isFailed ? 'danger' : 'info'}`}>
281
+ <a target="_blank" rel="noopener noreferrer" href={bakeryDetailUrl(stage)}>
282
+ View Bakery Details
283
+ </a>
284
+ </div>
285
+ </div>
286
+ </div>
287
+ )}
288
+ </ExecutionDetailsSection>
289
+ );
290
+ }
291
+
292
+ (DockerBakeExecutionDetails as any).title = 'bakeConfig';
293
+
294
+ export const DOCKER_BAKE_STAGE_CONFIG: any = {
295
+ provides: 'bake',
296
+ cloudProvider: 'docker',
297
+ label: 'Bake',
298
+ description: 'Bakes an image',
299
+ component: DockerBakeStageConfig,
300
+ executionDetailsSections: [DockerBakeExecutionDetails as any, ExecutionDetailsTasks],
301
+ executionLabelComponent: BakeExecutionLabel,
302
+ extraLabelLines: (stage: IStage) => {
303
+ return (stage as any).masterStage.context.allPreviouslyBaked ||
304
+ (stage as any).masterStage.context.somePreviouslyBaked
305
+ ? 1
306
+ : 0;
307
+ },
308
+ supportsCustomTimeout: true,
309
+ validators: [{ type: 'requiredField', fieldName: 'package' }],
310
+ restartable: true,
311
+ };
312
+
313
+ Registry.pipeline.registerStage(DOCKER_BAKE_STAGE_CONFIG);
@@ -45,8 +45,8 @@ Registry.pipeline.registerTrigger({
45
45
  });
46
46
 
47
47
  Registry.pipeline.registerTrigger({
48
- label: 'Helm Docker Registry',
49
- description: 'Executes the pipeline on an helm/image update',
48
+ label: 'Docker Registry (OCI)',
49
+ description: 'Executes the pipeline on a Docker OCI image update',
50
50
  key: 'helm/oci',
51
51
  component: DockerHelmOciTriggerConfig,
52
52
  manualExecutionComponent: DockerTriggerTemplate,
@@ -0,0 +1,124 @@
1
+ import { shallow } from 'enzyme';
2
+ import React from 'react';
3
+
4
+ import { DockerTriggerTemplate } from './DockerTriggerTemplate';
5
+ import { DockerImageReader } from '../../image';
6
+
7
+ interface IDeferred<T> {
8
+ promise: Promise<T>;
9
+ resolve: (value: T) => void;
10
+ }
11
+
12
+ function deferred<T>(): IDeferred<T> {
13
+ let resolve: (value: T) => void;
14
+ const promise = new Promise<T>((promiseResolve) => (resolve = promiseResolve));
15
+ return { promise, resolve };
16
+ }
17
+
18
+ describe('<DockerTriggerTemplate/>', () => {
19
+ it('formats Docker trigger labels', async () => {
20
+ await expectAsync(
21
+ Promise.resolve(
22
+ DockerTriggerTemplate.formatLabel({ account: 'prod-registry', repository: 'example/service' } as any),
23
+ ),
24
+ ).toBeResolvedTo('(Docker Registry) prod-registry: example/service');
25
+ });
26
+
27
+ it('writes docker image artifacts using tag references', () => {
28
+ const updateCommand = jasmine.createSpy('updateCommand');
29
+ const component = new DockerTriggerTemplate({
30
+ command: {
31
+ trigger: {
32
+ type: 'docker',
33
+ registry: 'registry.example.com',
34
+ repository: 'example/service',
35
+ },
36
+ },
37
+ updateCommand,
38
+ } as any);
39
+
40
+ (component as any).updateArtifact((component.props as any).command, '1.260101.000000-0000000');
41
+
42
+ expect(updateCommand).toHaveBeenCalledWith('extraFields.tag', '1.260101.000000-0000000');
43
+ expect(updateCommand).toHaveBeenCalledWith('extraFields.artifacts', [
44
+ {
45
+ type: 'docker/image',
46
+ name: 'registry.example.com/example/service',
47
+ version: '1.260101.000000-0000000',
48
+ reference: 'registry.example.com/example/service:1.260101.000000-0000000',
49
+ },
50
+ ]);
51
+ });
52
+
53
+ it('writes Helm OCI image artifacts using digest references', () => {
54
+ const updateCommand = jasmine.createSpy('updateCommand');
55
+ const component = new DockerTriggerTemplate({
56
+ command: {
57
+ trigger: {
58
+ type: 'helm/oci',
59
+ registry: 'registry.example.com',
60
+ repository: 'charts/service',
61
+ },
62
+ },
63
+ updateCommand,
64
+ } as any);
65
+ (component as any).state.lookupType = 'digest';
66
+
67
+ (component as any).updateArtifact((component.props as any).command, 'sha256:abc123');
68
+
69
+ expect(updateCommand).toHaveBeenCalledWith('extraFields.tag', 'sha256:abc123');
70
+ expect(updateCommand).toHaveBeenCalledWith('extraFields.artifacts', [
71
+ {
72
+ type: 'helm/image',
73
+ name: 'registry.example.com/charts/service',
74
+ version: 'sha256:abc123',
75
+ reference: 'registry.example.com/charts/service@sha256:abc123',
76
+ },
77
+ ]);
78
+ });
79
+ it('aborts superseded and unmounted tag queries without publishing cancellation errors', async () => {
80
+ jasmine.clock().install();
81
+ try {
82
+ const firstRequest = deferred<string[]>();
83
+ const secondRequest = deferred<string[]>();
84
+ const findTags = spyOn(DockerImageReader, 'findTags').and.returnValues(
85
+ firstRequest.promise,
86
+ secondRequest.promise,
87
+ );
88
+ const wrapper = shallow(
89
+ <DockerTriggerTemplate
90
+ command={{
91
+ trigger: { type: 'docker', repository: 'example/service' },
92
+ }}
93
+ updateCommand={jasmine.createSpy('updateCommand')}
94
+ />,
95
+ { disableLifecycleMethods: true },
96
+ );
97
+ const component = wrapper.instance() as DockerTriggerTemplate;
98
+ const tagLoadSuccess = spyOn(component as any, 'tagLoadSuccess').and.callThrough();
99
+ const tagLoadFailure = spyOn(component as any, 'tagLoadFailure').and.callThrough();
100
+
101
+ (component as any).initialize();
102
+ jasmine.clock().tick(250);
103
+ (component as any).searchTags();
104
+ jasmine.clock().tick(250);
105
+ const firstSignal = findTags.calls.argsFor(0)[1] as AbortSignal;
106
+ const secondSignal = findTags.calls.argsFor(1)[1] as AbortSignal;
107
+
108
+ expect(firstSignal.aborted).toBe(true);
109
+ expect(secondSignal.aborted).toBe(false);
110
+ wrapper.unmount();
111
+ expect(secondSignal.aborted).toBe(true);
112
+
113
+ firstRequest.resolve(['stale']);
114
+ secondRequest.resolve(['late']);
115
+ await Promise.all([firstRequest.promise, secondRequest.promise]);
116
+ await Promise.resolve();
117
+
118
+ expect(tagLoadSuccess).not.toHaveBeenCalled();
119
+ expect(tagLoadFailure).not.toHaveBeenCalled();
120
+ } finally {
121
+ jasmine.clock().uninstall();
122
+ }
123
+ });
124
+ });