Skip to content

Commit 511594f

Browse files
kapral18claude
andcommitted
[Snapshot Restore] Migrate flaky repository_add integration test to unit tests (#258974)
Closes #248548 - Migrates flaky `repository_add` client integration test to focused unit tests spread across the source files that own the functionality. - Removes the legacy `__jest__/client_integration/repository_add.test.ts` integration test. - Unskips the previously flaky settings validation test (now a pure function test — no timeouts possible). - The old integration test rendered the full app through a router with async HTTP mock layers, causing flaky 5000ms timeouts on CI (issue - The new unit tests render components directly with minimal mocks, use synchronous `fireEvent`, and test validation as pure function calls. - [Integration → unit test mapping (gist)](https://gist.github.com/kapral18/b6ccde9cbf84bc18c35fc356861aba50) - `node scripts/check_changes.ts` — passed - `yarn test:jest x-pack/platform/plugins/private/snapshot_restore/public/application/services/validation/validate_repository.test.ts` — 24 passed - `yarn test:jest x-pack/platform/plugins/private/snapshot_restore/public/application/components/repository_form/step_one.test.tsx` — 10 passed - `yarn test:jest x-pack/platform/plugins/private/snapshot_restore/public/application/components/repository_form/repository_form.test.tsx` — 14 passed - `yarn test:jest x-pack/platform/plugins/private/snapshot_restore/public/application/sections/repository_add/repository_add.test.tsx` — 1 passed Assisted with Claude Code using Claude 4.6 Opus Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> (cherry picked from commit 6b62324)
1 parent cccfe50 commit 511594f

9 files changed

Lines changed: 1054 additions & 647 deletions

File tree

x-pack/platform/plugins/private/snapshot_restore/__jest__/client_integration/repository_add.test.ts

Lines changed: 0 additions & 639 deletions
This file was deleted.

x-pack/platform/plugins/private/snapshot_restore/public/application/components/repository_form/repository_form.test.tsx

Lines changed: 419 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import React from 'react';
9+
import { fireEvent, render, screen } from '@testing-library/react';
10+
import { I18nProvider } from '@kbn/i18n-react';
11+
12+
import { i18n } from '@kbn/i18n';
13+
14+
import type { Repository, EmptyRepository, RepositoryType } from '../../../../common/types';
15+
import { textService } from '../../services/text';
16+
import { RepositoryFormStepOne } from './step_one';
17+
18+
/** Build a test repository object, bypassing strict union settings types. */
19+
const testRepo = (overrides: Record<string, unknown>) =>
20+
overrides as unknown as Repository | EmptyRepository;
21+
22+
const repositoryTypes: RepositoryType[] = ['fs', 'url', 'source', 'azure', 'gcs', 's3', 'hdfs'];
23+
24+
const mockUseLoadRepositoryTypes = jest.fn();
25+
26+
jest.mock('../../services/http', () => {
27+
const actual = jest.requireActual<typeof import('../../services/http')>('../../services/http');
28+
return {
29+
...actual,
30+
useLoadRepositoryTypes: (...args: unknown[]) => mockUseLoadRepositoryTypes(...args),
31+
};
32+
});
33+
34+
jest.mock('../../app_context', () => {
35+
const actual = jest.requireActual<typeof import('../../app_context')>('../../app_context');
36+
37+
return {
38+
...actual,
39+
useCore: () => ({
40+
docLinks: {
41+
links: {
42+
plugins: {
43+
snapshotRestoreRepos: 'https://doc-link',
44+
s3Repo: 'https://doc-link',
45+
hdfsRepo: 'https://doc-link',
46+
azureRepo: 'https://doc-link',
47+
gcsRepo: 'https://doc-link',
48+
},
49+
snapshotRestore: {
50+
registerSharedFileSystem: 'https://doc-link',
51+
registerUrl: 'https://doc-link',
52+
registerSourceOnly: 'https://doc-link',
53+
guide: 'https://doc-link',
54+
},
55+
},
56+
},
57+
}),
58+
};
59+
});
60+
61+
textService.setup(i18n);
62+
63+
const defaultProps: {
64+
repository: Repository | EmptyRepository;
65+
onNext: jest.Mock;
66+
updateRepository: jest.Mock;
67+
validation: { isValid: boolean; errors: Record<string, string[]> };
68+
} = {
69+
repository: { name: '', type: null, settings: {} },
70+
onNext: jest.fn(),
71+
updateRepository: jest.fn(),
72+
validation: { isValid: true, errors: {} },
73+
};
74+
75+
const renderStepOne = (overrides: Partial<typeof defaultProps> = {}) => {
76+
return render(
77+
<I18nProvider>
78+
<RepositoryFormStepOne {...defaultProps} {...overrides} />
79+
</I18nProvider>
80+
);
81+
};
82+
83+
describe('<RepositoryFormStepOne />', () => {
84+
beforeEach(() => {
85+
jest.clearAllMocks();
86+
mockUseLoadRepositoryTypes.mockReturnValue({
87+
isLoading: false,
88+
error: null,
89+
data: repositoryTypes,
90+
});
91+
});
92+
93+
describe('WHEN repository types are loaded', () => {
94+
it('SHOULD render a card for each repository type', () => {
95+
renderStepOne();
96+
97+
repositoryTypes.forEach((type) => {
98+
expect(screen.getByTestId(`${type}RepositoryType`)).toBeInTheDocument();
99+
});
100+
});
101+
});
102+
103+
describe('WHEN repository types are loading', () => {
104+
it('SHOULD show a loading indicator', () => {
105+
mockUseLoadRepositoryTypes.mockReturnValue({
106+
isLoading: true,
107+
error: null,
108+
data: [],
109+
});
110+
111+
renderStepOne();
112+
113+
expect(screen.getByTestId('sectionLoading')).toHaveTextContent('Loading repository types…');
114+
});
115+
});
116+
117+
describe('WHEN repository types are empty', () => {
118+
it('SHOULD show a no-repository-types error callout', () => {
119+
mockUseLoadRepositoryTypes.mockReturnValue({
120+
isLoading: false,
121+
error: null,
122+
data: [],
123+
});
124+
125+
renderStepOne();
126+
127+
expect(screen.getByTestId('noRepositoryTypesError')).toHaveTextContent(
128+
'No repository types available'
129+
);
130+
});
131+
});
132+
133+
describe('WHEN the next button is clicked', () => {
134+
it('SHOULD call onNext', () => {
135+
const onNext = jest.fn();
136+
renderStepOne({ onNext });
137+
138+
fireEvent.click(screen.getByTestId('nextButton'));
139+
140+
expect(onNext).toHaveBeenCalledTimes(1);
141+
});
142+
});
143+
144+
describe('WHEN a repository type card is clicked', () => {
145+
it('SHOULD call updateRepository with the selected type', () => {
146+
const updateRepository = jest.fn();
147+
renderStepOne({ updateRepository });
148+
149+
fireEvent.click(screen.getByTestId('fsRepositoryType'));
150+
151+
expect(updateRepository).toHaveBeenCalledWith({
152+
type: 'fs',
153+
settings: {},
154+
});
155+
});
156+
});
157+
158+
describe('WHEN source-only toggle is enabled', () => {
159+
it('SHOULD call updateRepository with source type and delegateType', () => {
160+
const updateRepository = jest.fn();
161+
renderStepOne({
162+
updateRepository,
163+
repository: testRepo({ name: 'test', type: 'fs', settings: {} }),
164+
});
165+
166+
fireEvent.click(screen.getByTestId('sourceOnlyToggle'));
167+
168+
expect(updateRepository).toHaveBeenCalledWith({
169+
type: 'source',
170+
settings: {
171+
delegateType: 'fs',
172+
},
173+
});
174+
});
175+
});
176+
177+
describe('WHEN source-only toggle is disabled', () => {
178+
it('SHOULD call updateRepository reverting to the delegate type', () => {
179+
const updateRepository = jest.fn();
180+
renderStepOne({
181+
updateRepository,
182+
repository: testRepo({ name: 'test', type: 'source', settings: { delegateType: 'fs' } }),
183+
});
184+
185+
fireEvent.click(screen.getByTestId('sourceOnlyToggle'));
186+
187+
expect(updateRepository).toHaveBeenCalledWith({
188+
type: 'fs',
189+
settings: {},
190+
});
191+
});
192+
});
193+
194+
describe('WHEN validation errors exist', () => {
195+
it('SHOULD display name validation error', () => {
196+
renderStepOne({
197+
validation: {
198+
isValid: false,
199+
errors: { name: ['Repository name is required.'] },
200+
},
201+
});
202+
203+
expect(screen.getByText('Repository name is required.')).toBeInTheDocument();
204+
});
205+
206+
it('SHOULD display type validation error', () => {
207+
renderStepOne({
208+
validation: {
209+
isValid: false,
210+
errors: { type: ['Type is required.'] },
211+
},
212+
});
213+
214+
expect(screen.getByText('Type is required.')).toBeInTheDocument();
215+
});
216+
217+
it('SHOULD display a validation error callout', () => {
218+
renderStepOne({
219+
validation: {
220+
isValid: false,
221+
errors: { name: ['Repository name is required.'] },
222+
},
223+
});
224+
225+
expect(screen.getByTestId('repositoryFormError')).toBeInTheDocument();
226+
});
227+
});
228+
});

x-pack/platform/plugins/private/snapshot_restore/public/application/components/repository_form/step_one.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ import {
2424
EuiTitle,
2525
} from '@elastic/eui';
2626

27+
import type { Error } from '@kbn/es-ui-shared-plugin/public';
28+
import { SectionError } from '@kbn/es-ui-shared-plugin/public';
29+
2730
import type { Repository, RepositoryType, EmptyRepository } from '../../../../common/types';
2831
import { REPOSITORY_TYPES } from '../../../../common';
29-
import type { Error } from '../../../shared_imports';
30-
import { SectionError } from '../../../shared_imports';
3132

3233
import { useLoadRepositoryTypes } from '../../services/http';
3334
import { textService } from '../../services/text';
3435
import type { RepositoryValidation } from '../../services/validation';
35-
import { SectionLoading, RepositoryTypeLogo } from '..';
36+
import { SectionLoading } from '../loading';
37+
import { RepositoryTypeLogo } from '../repository_type_logo';
3638
import { useCore } from '../../app_context';
3739
import { getRepositoryTypeDocUrl } from '../../lib/type_to_doc_url';
3840

x-pack/platform/plugins/private/snapshot_restore/public/application/components/repository_form/type_settings/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
import React from 'react';
99
import { FormattedMessage } from '@kbn/i18n-react';
1010

11+
import { SectionError } from '@kbn/es-ui-shared-plugin/public';
12+
1113
import { REPOSITORY_TYPES } from '../../../../../common';
1214
import type { Repository, RepositoryType, EmptyRepository } from '../../../../../common/types';
13-
import { SectionError } from '../../../../shared_imports';
1415
import { useServices } from '../../../app_context';
1516
import type { RepositorySettingsValidation } from '../../../services/validation';
1617

0 commit comments

Comments
 (0)