Skip to content

Commit 1c28d4a

Browse files
committed
test(observability): add unit tests for delete tenant action
ref: #MAOBS-105 Signed-off-by: yjaaouane <[email protected]>
1 parent 6931b7c commit 1c28d4a

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React from 'react';
2+
3+
import { render, screen } from '@testing-library/react';
4+
import userEvent from '@testing-library/user-event';
5+
import { vi } from 'vitest';
6+
7+
import { ConfirmationModal } from '@/components/listing/common/confirmation-modal/ConfirmationModal.component';
8+
9+
vi.mock('@ovh-ux/manager-react-components', () => ({
10+
Modal: ({
11+
children,
12+
'data-testid': dataTestId,
13+
...props
14+
}: {
15+
children: React.ReactNode;
16+
'data-testid'?: string;
17+
[key: string]: unknown;
18+
}) => (
19+
<div data-testid={dataTestId || 'modal'} {...props}>
20+
{children}
21+
</div>
22+
),
23+
}));
24+
25+
describe('ConfirmationModal', () => {
26+
it('renders title and message', () => {
27+
render(
28+
<ConfirmationModal
29+
title="Delete tenant"
30+
message="Are you sure?"
31+
onDismiss={() => {}}
32+
confirmButtonLabel="Confirm"
33+
cancelButtonLabel="Cancel"
34+
/>,
35+
);
36+
37+
expect(screen.getByTestId('confirmation-modal')).toBeInTheDocument();
38+
expect(screen.getByText('Delete tenant')).toBeInTheDocument();
39+
expect(screen.getByText('Are you sure?')).toBeInTheDocument();
40+
expect(screen.getByTestId('confirm-button-test-id')).toBeInTheDocument();
41+
expect(screen.getByTestId('cancel-button-test-id')).toBeInTheDocument();
42+
});
43+
44+
it('shows spinner and hides actions when loading', () => {
45+
render(
46+
<ConfirmationModal title="Processing" message="Please wait" onDismiss={() => {}} isLoading />,
47+
);
48+
49+
expect(screen.getByTestId('spinner')).toBeInTheDocument();
50+
expect(screen.queryByTestId('confirm-button-test-id')).not.toBeInTheDocument();
51+
expect(screen.queryByTestId('cancel-button-test-id')).not.toBeInTheDocument();
52+
});
53+
54+
it('displays error message when provided', () => {
55+
render(
56+
<ConfirmationModal
57+
title="Error"
58+
message="Something went wrong"
59+
onDismiss={() => {}}
60+
error="Could not delete"
61+
/>,
62+
);
63+
64+
expect(screen.getByTestId('confirmation-modal-error-message')).toBeInTheDocument();
65+
expect(screen.getByText('Could not delete')).toBeInTheDocument();
66+
});
67+
68+
it('calls handlers when clicking buttons', async () => {
69+
const user = userEvent.setup();
70+
const onDismiss = vi.fn();
71+
const onConfirm = vi.fn();
72+
73+
render(
74+
<ConfirmationModal
75+
title="Confirm"
76+
message="Confirm action"
77+
onDismiss={onDismiss}
78+
onConfirm={onConfirm}
79+
confirmButtonLabel="Confirm"
80+
cancelButtonLabel="Cancel"
81+
/>,
82+
);
83+
84+
await user.click(screen.getByTestId('cancel-button-test-id'));
85+
expect(onDismiss).toHaveBeenCalledTimes(1);
86+
87+
await user.click(screen.getByTestId('confirm-button-test-id'));
88+
expect(onConfirm).toHaveBeenCalledTimes(1);
89+
});
90+
91+
it('respects confirm button loading and disabled states', () => {
92+
render(
93+
<ConfirmationModal
94+
title="States"
95+
message="Check states"
96+
onDismiss={() => {}}
97+
onConfirm={() => {}}
98+
confirmButtonLabel="Confirm"
99+
isConfirmButtonDisabled
100+
isConfirmButtonLoading
101+
/>,
102+
);
103+
104+
const confirm = screen.getByTestId('confirm-button-test-id');
105+
expect(confirm).toBeInTheDocument();
106+
});
107+
});

packages/manager/apps/observability/src/__tests__/components/listing/tenants/actions/TenantsListActions.component.spec.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ vi.mock('@ovh-ux/manager-react-components', () => ({
9090
ActionMenuItem: {},
9191
}));
9292

93+
vi.mock('@/routes/Routes.utils', () => ({
94+
getDeleteTenantUrl: (tenantId: string) => `/metrics/tenants/delete/${tenantId}`,
95+
}));
96+
9397
describe('TenantsListActions', () => {
9498
const testTenantId: string = 'test-tenant-123';
9599

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React from 'react';
2+
3+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4+
import { renderHook, waitFor } from '@testing-library/react';
5+
import { vi } from 'vitest';
6+
7+
import { deleteTenant } from '@/data/api/tenants.api';
8+
import type { GetTenantPayload } from '@/data/api/tenants.props';
9+
import { useDeleteTenant } from '@/data/hooks/tenants/useDeleteTenant.hook';
10+
import type { Tenant } from '@/types/tenants.type';
11+
12+
vi.mock('@/data/api/tenants.api', () => ({
13+
deleteTenant: vi.fn(),
14+
}));
15+
16+
const mockDeleteTenant = vi.mocked(deleteTenant);
17+
18+
const createWrapper = () => {
19+
const queryClient = new QueryClient({
20+
defaultOptions: {
21+
queries: { retry: false, gcTime: 0 },
22+
mutations: { retry: false, gcTime: 0 },
23+
},
24+
});
25+
26+
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
27+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
28+
);
29+
TestWrapper.displayName = 'TestWrapper';
30+
return { TestWrapper, queryClient };
31+
};
32+
33+
describe('useDeleteTenant', () => {
34+
beforeEach(() => {
35+
vi.clearAllMocks();
36+
});
37+
38+
const payload: GetTenantPayload = {
39+
resourceName: 'service-a',
40+
} as unknown as GetTenantPayload;
41+
42+
const deletedTenant: Tenant = {
43+
id: 'tenant-123',
44+
currentState: {
45+
title: 'Tenant 123',
46+
},
47+
};
48+
49+
it('calls deleteTenant with correct payload', async () => {
50+
mockDeleteTenant.mockResolvedValue(deletedTenant);
51+
const { TestWrapper } = createWrapper();
52+
53+
const { result } = renderHook(() => useDeleteTenant(), { wrapper: TestWrapper });
54+
55+
result.current.mutate(payload);
56+
57+
await waitFor(() => {
58+
expect(mockDeleteTenant).toHaveBeenCalledWith(payload);
59+
});
60+
});
61+
62+
it('returns success state on resolve', async () => {
63+
mockDeleteTenant.mockResolvedValue(deletedTenant);
64+
const { TestWrapper } = createWrapper();
65+
66+
const { result } = renderHook(() => useDeleteTenant(), { wrapper: TestWrapper });
67+
68+
result.current.mutate(payload);
69+
70+
await waitFor(() => {
71+
expect(result.current.isSuccess).toBe(true);
72+
expect(result.current.data).toEqual(deletedTenant);
73+
expect(result.current.error).toBe(null);
74+
});
75+
});
76+
77+
it('returns error state on reject', async () => {
78+
const err = new Error('Failed');
79+
mockDeleteTenant.mockRejectedValue(err);
80+
const { TestWrapper } = createWrapper();
81+
82+
const { result } = renderHook(() => useDeleteTenant(), { wrapper: TestWrapper });
83+
84+
result.current.mutate(payload);
85+
86+
await waitFor(() => {
87+
expect(result.current.isError).toBe(true);
88+
expect(result.current.error).toEqual(err);
89+
expect(result.current.data).toBeUndefined();
90+
});
91+
});
92+
93+
it('supports custom lifecycle callbacks', async () => {
94+
mockDeleteTenant.mockResolvedValue(deletedTenant);
95+
const onSuccess = vi.fn();
96+
const onError = vi.fn();
97+
const onMutate = vi.fn();
98+
const onSettled = vi.fn();
99+
const { TestWrapper } = createWrapper();
100+
101+
const { result } = renderHook(
102+
() =>
103+
useDeleteTenant({
104+
onSuccess,
105+
onError,
106+
onMutate,
107+
onSettled,
108+
}),
109+
{ wrapper: TestWrapper },
110+
);
111+
112+
result.current.mutate(payload);
113+
114+
await waitFor(() => {
115+
expect(onMutate).toHaveBeenCalledWith(payload);
116+
expect(onSuccess).toHaveBeenCalled();
117+
expect(onSettled).toHaveBeenCalled();
118+
});
119+
120+
expect(onError).not.toHaveBeenCalled();
121+
});
122+
123+
it('exposes loading state during mutation', async () => {
124+
let resolveFn: (v: Tenant) => void;
125+
const pending = new Promise<Tenant>((resolve) => {
126+
resolveFn = resolve;
127+
});
128+
mockDeleteTenant.mockReturnValue(pending);
129+
const { TestWrapper } = createWrapper();
130+
131+
const { result } = renderHook(() => useDeleteTenant(), { wrapper: TestWrapper });
132+
133+
result.current.mutate(payload);
134+
135+
await waitFor(() => {
136+
expect(result.current.isPending).toBe(true);
137+
});
138+
expect(result.current.data).toBeUndefined();
139+
expect(result.current.error).toBe(null);
140+
141+
resolveFn!(deletedTenant);
142+
});
143+
144+
it('can reset mutation state', async () => {
145+
mockDeleteTenant.mockResolvedValue(deletedTenant);
146+
const { TestWrapper } = createWrapper();
147+
const { result } = renderHook(() => useDeleteTenant(), { wrapper: TestWrapper });
148+
149+
result.current.mutate(payload);
150+
await waitFor(() => expect(result.current.isSuccess).toBe(true));
151+
152+
result.current.reset();
153+
await waitFor(() => expect(result.current.isIdle).toBe(true));
154+
expect(result.current.data).toBeUndefined();
155+
expect(result.current.error).toBe(null);
156+
});
157+
});

0 commit comments

Comments
 (0)