Skip to content

Commit 4ca7bac

Browse files
Tests for resources
1 parent 4978b72 commit 4ca7bac

File tree

3 files changed

+340
-5
lines changed

3 files changed

+340
-5
lines changed

src/components/ControlPlane/ActionsMenu.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export type ActionsMenuProps<T> = {
1717
buttonIcon?: string;
1818
};
1919

20-
export function ActionsMenu<T>({ item, actions, buttonIcon = 'overflow' }: ActionsMenuProps<T>) {
20+
export function ActionsMenu<T>({ item, actions }: ActionsMenuProps<T>) {
2121
const popoverRef = useRef<MenuDomRef>(null);
2222
const [open, setOpen] = useState(false);
2323

@@ -30,10 +30,11 @@ export function ActionsMenu<T>({ item, actions, buttonIcon = 'overflow' }: Actio
3030

3131
return (
3232
<>
33-
<Button icon={buttonIcon} design="Transparent" onClick={handleOpenerClick} />
33+
<Button icon="overflow" design="Transparent" data-testid="ActionsMenu-opener" onClick={handleOpenerClick} />
3434
<Menu
3535
ref={popoverRef}
3636
open={open}
37+
data-component-name="ActionsMenu"
3738
onItemClick={(event) => {
3839
const element = event.detail.item as HTMLElement & { disabled?: boolean };
3940
const actionKey = element.dataset.actionKey;
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
/* eslint-disable @typescript-eslint/no-explicit-any */
2+
import { ManagedResources } from './ManagedResources.tsx';
3+
import { SplitterProvider } from '../Splitter/SplitterContext.tsx';
4+
import { ManagedResourceGroup } from '../../lib/shared/types.ts';
5+
import { MemoryRouter } from 'react-router-dom';
6+
import { useApiResourceMutation } from '../../lib/api/useApiResource.ts';
7+
import '@ui5/webcomponents-cypress-commands';
8+
import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
9+
10+
describe('ManagedResources - Delete Resource', () => {
11+
let deleteCalled = false;
12+
let patchCalled = false;
13+
let triggerCallCount = 0;
14+
15+
const fakeUseApiResourceMutation: typeof useApiResourceMutation = (): any => {
16+
return {
17+
data: undefined,
18+
error: undefined,
19+
reset: () => {},
20+
trigger: async (): Promise<any> => {
21+
const currentTriggerCall = triggerCallCount++;
22+
if (currentTriggerCall === 0) {
23+
deleteCalled = true;
24+
} else {
25+
patchCalled = true;
26+
}
27+
return undefined;
28+
},
29+
isMutating: false,
30+
};
31+
};
32+
33+
const mockManagedResources: ManagedResourceGroup[] = [
34+
{
35+
items: [
36+
{
37+
apiVersion: 'account.btp.sap.crossplane.io/v1alpha1',
38+
kind: 'Subaccount',
39+
metadata: {
40+
name: 'test-subaccount',
41+
namespace: 'test-namespace',
42+
creationTimestamp: '2024-01-01T00:00:00Z',
43+
resourceVersion: '1',
44+
labels: {},
45+
},
46+
spec: {},
47+
status: {
48+
conditions: [
49+
{
50+
type: 'Ready',
51+
status: 'True',
52+
lastTransitionTime: '2024-01-01T00:00:00Z',
53+
},
54+
{
55+
type: 'Synced',
56+
status: 'True',
57+
lastTransitionTime: '2024-01-01T00:00:00Z',
58+
},
59+
],
60+
},
61+
} as any,
62+
],
63+
},
64+
];
65+
66+
before(() => {
67+
// Set up interceptors once for all tests
68+
cy.intercept('GET', '**/managed', {
69+
statusCode: 200,
70+
body: mockManagedResources,
71+
}).as('getManagedResources');
72+
73+
cy.intercept('GET', '**/customresourcedefinitions*', {
74+
statusCode: 200,
75+
body: {
76+
items: [
77+
{
78+
spec: {
79+
names: {
80+
kind: 'Subaccount',
81+
plural: 'subaccounts',
82+
},
83+
},
84+
},
85+
],
86+
},
87+
}).as('getCRDs');
88+
});
89+
90+
beforeEach(() => {
91+
deleteCalled = false;
92+
patchCalled = false;
93+
triggerCallCount = 0;
94+
});
95+
96+
it('deletes a managed resource', () => {
97+
cy.mount(
98+
<MemoryRouter>
99+
<SplitterProvider>
100+
<ManagedResources useApiResourceMutation={fakeUseApiResourceMutation} />
101+
</SplitterProvider>
102+
</MemoryRouter>,
103+
);
104+
105+
cy.wait('@getManagedResources');
106+
cy.wait('@getCRDs');
107+
108+
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
109+
cy.wait(500);
110+
111+
cy.contains('test-subaccount').should('be.visible');
112+
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
113+
cy.contains('Delete').click({ force: true });
114+
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');
115+
116+
cy.then(() => cy.wrap(deleteCalled).should('equal', false));
117+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();
118+
cy.then(() => cy.wrap(deleteCalled).should('equal', true));
119+
});
120+
121+
it('force deletes a managed resource', () => {
122+
cy.mount(
123+
<MemoryRouter>
124+
<SplitterProvider>
125+
<ManagedResources useApiResourceMutation={fakeUseApiResourceMutation} />
126+
</SplitterProvider>
127+
</MemoryRouter>,
128+
);
129+
130+
cy.wait(1000);
131+
132+
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
133+
cy.wait(500);
134+
135+
cy.contains('test-subaccount').should('be.visible');
136+
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
137+
cy.contains('Delete').click({ force: true });
138+
139+
// Expand Advanced section
140+
cy.contains('Advanced').click();
141+
cy.wait(500);
142+
143+
// Click directly on "Force deletion" text - this should toggle the checkbox
144+
cy.contains('Force deletion').click({ force: true });
145+
cy.wait(500);
146+
147+
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');
148+
149+
cy.then(() => cy.wrap(deleteCalled).should('equal', false));
150+
cy.then(() => cy.wrap(patchCalled).should('equal', false));
151+
152+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();
153+
154+
cy.wait(2000);
155+
156+
cy.then(() => {
157+
cy.log(`deleteCalled: ${deleteCalled}, patchCalled: ${patchCalled}`);
158+
});
159+
160+
cy.then(() => cy.wrap(deleteCalled).should('equal', true));
161+
cy.then(() => cy.wrap(patchCalled).should('equal', true));
162+
});
163+
164+
it('keeps delete button disabled until confirmation text is entered', () => {
165+
// Setup interceptors for this test
166+
cy.intercept('GET', '**/managed', {
167+
statusCode: 200,
168+
body: mockManagedResources,
169+
}).as('getManagedResourcesValidation');
170+
171+
cy.intercept('GET', '**/customresourcedefinitions*', {
172+
statusCode: 200,
173+
body: {
174+
items: [
175+
{
176+
spec: {
177+
names: {
178+
kind: 'Subaccount',
179+
plural: 'subaccounts',
180+
},
181+
},
182+
},
183+
],
184+
},
185+
}).as('getCRDsValidation');
186+
187+
cy.mount(
188+
<MemoryRouter>
189+
<SplitterProvider>
190+
<ManagedResources useApiResourceMutation={fakeUseApiResourceMutation} />
191+
</SplitterProvider>
192+
</MemoryRouter>,
193+
);
194+
195+
cy.wait('@getManagedResourcesValidation');
196+
cy.wait('@getCRDsValidation');
197+
198+
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
199+
cy.wait(500);
200+
201+
cy.contains('test-subaccount').should('be.visible');
202+
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
203+
cy.contains('Delete').click({ force: true });
204+
205+
// Delete button should be disabled initially
206+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('have.attr', 'disabled');
207+
208+
// Type wrong text
209+
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('wrong-text');
210+
cy.wait(300);
211+
212+
// Delete button should still be disabled
213+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('have.attr', 'disabled');
214+
215+
// Clear input by selecting all and deleting
216+
cy.get('ui5-dialog[open]').find('ui5-input').find('input[id*="inner"]').clear({ force: true });
217+
cy.wait(300);
218+
219+
// Type correct text
220+
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input('test-subaccount');
221+
cy.wait(300);
222+
223+
// Delete button should now be enabled
224+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').should('not.have.attr', 'disabled');
225+
});
226+
});
227+
228+
describe('ManagedResources - Edit Resource', () => {
229+
let patchHandlerCreated = false;
230+
231+
const fakeUseHandleResourcePatch: typeof useHandleResourcePatch = () => {
232+
patchHandlerCreated = true;
233+
return async () => {
234+
return true;
235+
};
236+
};
237+
238+
const mockManagedResources: ManagedResourceGroup[] = [
239+
{
240+
items: [
241+
{
242+
apiVersion: 'account.btp.sap.crossplane.io/v1alpha1',
243+
kind: 'Subaccount',
244+
metadata: {
245+
name: 'test-subaccount',
246+
namespace: 'test-namespace',
247+
creationTimestamp: '2024-01-01T00:00:00Z',
248+
resourceVersion: '1',
249+
labels: {},
250+
},
251+
spec: {},
252+
status: {
253+
conditions: [
254+
{
255+
type: 'Ready',
256+
status: 'True',
257+
lastTransitionTime: '2024-01-01T00:00:00Z',
258+
},
259+
{
260+
type: 'Synced',
261+
status: 'True',
262+
lastTransitionTime: '2024-01-01T00:00:00Z',
263+
},
264+
],
265+
},
266+
} as any,
267+
],
268+
},
269+
];
270+
271+
before(() => {
272+
cy.intercept('GET', '**/managed', {
273+
statusCode: 200,
274+
body: mockManagedResources,
275+
}).as('getManagedResources');
276+
277+
cy.intercept('GET', '**/customresourcedefinitions*', {
278+
statusCode: 200,
279+
body: {
280+
items: [
281+
{
282+
spec: {
283+
names: {
284+
kind: 'Subaccount',
285+
plural: 'subaccounts',
286+
},
287+
},
288+
},
289+
],
290+
},
291+
}).as('getCRDs');
292+
});
293+
294+
beforeEach(() => {
295+
patchHandlerCreated = false;
296+
});
297+
298+
it('initializes patch handler and edit button is available', () => {
299+
cy.mount(
300+
<MemoryRouter>
301+
<SplitterProvider>
302+
<ManagedResources useHandleResourcePatch={fakeUseHandleResourcePatch} />
303+
</SplitterProvider>
304+
</MemoryRouter>,
305+
);
306+
307+
cy.wait('@getManagedResources');
308+
cy.wait('@getCRDs');
309+
310+
// Verify patch handler was initialized
311+
cy.then(() => cy.wrap(patchHandlerCreated).should('equal', true));
312+
313+
cy.get('button[aria-label*="xpand"]').first().click({ force: true });
314+
cy.wait(500);
315+
316+
cy.contains('test-subaccount').should('be.visible');
317+
cy.get('[data-testid="ActionsMenu-opener"]').first().click({ force: true });
318+
319+
// Verify Edit button exists
320+
cy.contains('Edit').should('exist');
321+
322+
// Verify Edit button is not disabled (check separately)
323+
cy.contains('Edit').should('not.have.attr', 'disabled');
324+
325+
// Click Edit button
326+
cy.contains('Edit').click({ force: true });
327+
});
328+
});

src/components/ControlPlane/ManagedResources.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Toolbar,
1010
ToolbarSpacer,
1111
} from '@ui5/webcomponents-react';
12-
import { useApiResource, useApiResourceMutation } from '../../lib/api/useApiResource';
12+
import { useApiResource, useApiResourceMutation as _useApiResourceMutation } from '../../lib/api/useApiResource';
1313
import { ManagedResourcesRequest } from '../../lib/api/types/crossplane/listManagedResources';
1414
import { formatDateAsTimeAgo } from '../../utils/i18n/timeAgo';
1515
import IllustratedError from '../Shared/IllustratedError';
@@ -34,7 +34,7 @@ import { useSplitter } from '../Splitter/SplitterContext.tsx';
3434
import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx';
3535
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
3636
import { APIError } from '../../lib/api/error.ts';
37-
import { useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
37+
import { useHandleResourcePatch as _useHandleResourcePatch } from '../../lib/api/types/crossplane/useHandleResourcePatch.ts';
3838

3939
interface StatusFilterColumn {
4040
filterValue?: string;
@@ -54,7 +54,13 @@ type ResourceRow = {
5454
conditionSyncedMessage: string;
5555
};
5656

57-
export function ManagedResources() {
57+
export function ManagedResources({
58+
useApiResourceMutation = _useApiResourceMutation,
59+
useHandleResourcePatch = _useHandleResourcePatch,
60+
}: {
61+
useApiResourceMutation?: typeof _useApiResourceMutation;
62+
useHandleResourcePatch?: typeof _useHandleResourcePatch;
63+
} = {}) {
5864
const { t } = useTranslation();
5965
const toast = useToast();
6066
const { openInAside } = useSplitter();

0 commit comments

Comments
 (0)