Skip to content

Commit 1b5dbf0

Browse files
test: create/delete project (#355)
1 parent b9a20e2 commit 1b5dbf0

File tree

8 files changed

+489
-32
lines changed

8 files changed

+489
-32
lines changed
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { CreateProjectDialogContainer } from './CreateProjectDialogContainer';
2+
import { useCreateProject, CreateProjectParams } from '../../hooks/useCreateProject';
3+
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding';
4+
5+
describe('CreateProjectDialogContainer', () => {
6+
let createProjectPayload: CreateProjectParams | null = null;
7+
8+
const fakeUseCreateProject: typeof useCreateProject = () => ({
9+
createProject: async (data: CreateProjectParams): Promise<void> => {
10+
createProjectPayload = data;
11+
},
12+
isLoading: false,
13+
});
14+
15+
const fakeUseAuthOnboarding = (() => ({
16+
user: {
17+
email: 'name@domain.com',
18+
},
19+
})) as typeof useAuthOnboarding;
20+
21+
beforeEach(() => {
22+
createProjectPayload = null;
23+
});
24+
25+
it('creates a project with valid data', () => {
26+
const setIsOpen = cy.stub();
27+
28+
cy.mount(
29+
<CreateProjectDialogContainer
30+
useCreateProject={fakeUseCreateProject}
31+
useAuthOnboarding={fakeUseAuthOnboarding}
32+
isOpen={true}
33+
setIsOpen={setIsOpen}
34+
/>,
35+
);
36+
37+
const expectedPayload = {
38+
name: 'test-project',
39+
displayName: 'Test Project Display Name',
40+
chargingTarget: '12345678-1234-1234-1234-123456789abc',
41+
chargingTargetType: 'btp',
42+
members: [
43+
{
44+
name: 'name@domain.com',
45+
roles: ['admin'],
46+
kind: 'User',
47+
},
48+
],
49+
};
50+
51+
// Fill in the form
52+
cy.get('#name').find('input[id*="inner"]').type('test-project');
53+
cy.get('#displayName').find('input[id*="inner"]').type('Test Project Display Name');
54+
55+
// Select charging target type (should be pre-selected as 'btp')
56+
cy.get('#chargingTargetType').click();
57+
cy.contains('BTP').click();
58+
59+
// Fill charging target
60+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
61+
62+
// Submit the form
63+
cy.get('ui5-button').contains('Create').click();
64+
65+
// Verify the hook was called with correct data
66+
cy.then(() => cy.wrap(createProjectPayload).deepEqualJson(expectedPayload));
67+
68+
// Dialog should close on success
69+
cy.wrap(setIsOpen).should('have.been.calledWith', false);
70+
});
71+
72+
it('validates required fields', () => {
73+
const setIsOpen = cy.stub();
74+
75+
cy.mount(
76+
<CreateProjectDialogContainer
77+
useCreateProject={fakeUseCreateProject}
78+
useAuthOnboarding={fakeUseAuthOnboarding}
79+
isOpen={true}
80+
setIsOpen={setIsOpen}
81+
/>,
82+
);
83+
84+
// Try to submit without filling required fields
85+
cy.get('ui5-button').contains('Create').click();
86+
87+
// Should show validation errors
88+
cy.get('#name').should('have.attr', 'value-state', 'Negative');
89+
cy.contains('This field is required').should('exist');
90+
91+
// Dialog should not close
92+
cy.wrap(setIsOpen).should('not.have.been.called');
93+
});
94+
95+
it('validates charging target format for BTP', () => {
96+
const setIsOpen = cy.stub();
97+
98+
cy.mount(
99+
<CreateProjectDialogContainer
100+
useCreateProject={fakeUseCreateProject}
101+
useAuthOnboarding={fakeUseAuthOnboarding}
102+
isOpen={true}
103+
setIsOpen={setIsOpen}
104+
/>,
105+
);
106+
107+
cy.get('#name').find('input[id*="inner"]').type('test-project');
108+
cy.get('#chargingTargetType').click();
109+
cy.contains('BTP').click();
110+
111+
// Invalid format
112+
cy.get('#chargingTarget').find('input[id*="inner"]').type('invalid-format');
113+
cy.get('ui5-button').contains('Create').click();
114+
115+
// Should show validation error
116+
cy.get('#chargingTarget').should('have.attr', 'value-state', 'Negative');
117+
118+
// Dialog should not close
119+
cy.wrap(setIsOpen).should('not.have.been.called');
120+
});
121+
122+
it('should not close dialog when creation fails', () => {
123+
const failingUseCreateProject: typeof useCreateProject = () => ({
124+
createProject: async (): Promise<void> => {
125+
throw new Error('Creation failed');
126+
},
127+
isLoading: false,
128+
});
129+
130+
const setIsOpen = cy.stub();
131+
132+
cy.mount(
133+
<CreateProjectDialogContainer
134+
useCreateProject={failingUseCreateProject}
135+
useAuthOnboarding={fakeUseAuthOnboarding}
136+
isOpen={true}
137+
setIsOpen={setIsOpen}
138+
/>,
139+
);
140+
141+
// Fill in the form
142+
cy.get('#name').find('input[id*="inner"]').type('test-project');
143+
cy.get('#chargingTargetType').click();
144+
cy.contains('BTP').click();
145+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
146+
147+
// Submit the form
148+
cy.get('ui5-button').contains('Create').click();
149+
150+
// Dialog should NOT close on failure
151+
cy.wrap(setIsOpen).should('not.have.been.called');
152+
153+
// Dialog should still be visible
154+
cy.contains('Create').should('be.visible');
155+
});
156+
});

src/components/Dialogs/CreateProjectDialogContainer.tsx

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
import { useCallback, useEffect, useMemo, useRef } from 'react';
2-
import { useApiResourceMutation } from '../../lib/api/useApiResource';
32
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
43
import { APIError } from '../../lib/api/error';
54
import { CreateProjectWorkspaceDialog, OnCreatePayload } from './CreateProjectWorkspaceDialog.tsx';
6-
7-
import { useToast } from '../../context/ToastContext.tsx';
8-
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
5+
import { useAuthOnboarding as _useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
96
import { MemberRoles } from '../../lib/api/types/shared/members.ts';
10-
117
import { useTranslation } from 'react-i18next';
128
import { zodResolver } from '@hookform/resolvers/zod';
139
import { useForm } from 'react-hook-form';
14-
import { CreateProject, CreateProjectResource, CreateProjectType } from '../../lib/api/types/crate/createProject.ts';
1510
import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts';
1611
import { CreateDialogProps } from './CreateWorkspaceDialogContainer.tsx';
12+
import { useCreateProject as _useCreateProject } from '../../hooks/useCreateProject.ts';
1713

1814
export function CreateProjectDialogContainer({
1915
isOpen,
2016
setIsOpen,
17+
useCreateProject = _useCreateProject,
18+
useAuthOnboarding = _useAuthOnboarding,
2119
}: {
2220
isOpen: boolean;
2321
setIsOpen: (isOpen: boolean) => void;
22+
useCreateProject?: typeof _useCreateProject;
23+
useAuthOnboarding?: typeof _useAuthOnboarding;
2424
}) {
2525
const { t } = useTranslation();
2626
const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]);
@@ -44,6 +44,9 @@ export function CreateProjectDialogContainer({
4444
const { user } = useAuthOnboarding();
4545

4646
const username = user?.email;
47+
const { createProject } = useCreateProject();
48+
const errorDialogRef = useRef<ErrorDialogHandle>(null);
49+
4750
const clearForm = useCallback(() => {
4851
resetField('name');
4952
resetField('chargingTarget');
@@ -60,12 +63,6 @@ export function CreateProjectDialogContainer({
6063
}
6164
}, [resetField, setValue, username, isOpen, clearForm]);
6265

63-
const toast = useToast();
64-
65-
const { trigger } = useApiResourceMutation<CreateProjectType>(CreateProjectResource());
66-
67-
const errorDialogRef = useRef<ErrorDialogHandle>(null);
68-
6966
const handleProjectCreate = async ({
7067
name,
7168
chargingTarget,
@@ -74,16 +71,14 @@ export function CreateProjectDialogContainer({
7471
members,
7572
}: OnCreatePayload): Promise<boolean> => {
7673
try {
77-
await trigger(
78-
CreateProject(name, {
79-
displayName: displayName,
80-
chargingTarget: chargingTarget,
81-
members: members,
82-
chargingTargetType: chargingTargetType,
83-
}),
84-
);
74+
await createProject({
75+
name,
76+
displayName,
77+
chargingTarget,
78+
chargingTargetType,
79+
members,
80+
});
8581
setIsOpen(false);
86-
toast.show(t('CreateProjectDialog.toastMessage'));
8782
return true;
8883
} catch (e) {
8984
console.error(e);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { ProjectsListItemMenu } from './ProjectsListItemMenu.tsx';
2+
import { useDeleteProject } from '../../hooks/useDeleteProject.ts';
3+
import '@ui5/webcomponents-cypress-commands';
4+
5+
describe('ProjectsListItemMenu', () => {
6+
let deleteProjectCalled = false;
7+
8+
const fakeUseDeleteProject: typeof useDeleteProject = () => ({
9+
deleteProject: async (): Promise<void> => {
10+
deleteProjectCalled = true;
11+
},
12+
});
13+
14+
beforeEach(() => {
15+
deleteProjectCalled = false;
16+
});
17+
18+
it('deletes the project', () => {
19+
const projectName = 'test-project';
20+
21+
cy.mount(<ProjectsListItemMenu projectName={projectName} useDeleteProject={fakeUseDeleteProject} />);
22+
23+
// Open overflow menu
24+
cy.get('ui5-button[icon="overflow"]').click();
25+
26+
// Click delete option
27+
cy.contains('Delete project').click({ force: true });
28+
29+
// Type confirmation text
30+
cy.get('ui5-dialog[open]').find('ui5-input').typeIntoUi5Input(projectName);
31+
32+
// Verify delete not called yet
33+
cy.then(() => cy.wrap(deleteProjectCalled).should('equal', false));
34+
35+
// Click delete button
36+
cy.get('ui5-dialog[open]').find('ui5-button').contains('Delete').click();
37+
38+
// Verify delete was called
39+
cy.then(() => cy.wrap(deleteProjectCalled).should('equal', true));
40+
});
41+
});

src/components/Projects/ProjectsListItemMenu.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,25 @@ import '@ui5/webcomponents-icons/dist/accept';
77
import { useTranslation } from 'react-i18next';
88
import { DeleteConfirmationDialog } from '../Dialogs/DeleteConfirmationDialog.tsx';
99

10-
import { useToast } from '../../context/ToastContext.tsx';
11-
import { useApiResourceMutation } from '../../lib/api/useApiResource.ts';
12-
import { DeleteWorkspaceType } from '../../lib/api/types/crate/deleteWorkspace.ts';
13-
import { DeleteProjectResource } from '../../lib/api/types/crate/deleteProject.ts';
10+
import { useDeleteProject as _useDeleteProject } from '../../hooks/useDeleteProject.ts';
1411
import { KubectlDeleteProject } from '../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteProject.tsx';
1512

1613
type ProjectsListItemMenuProps = {
1714
projectName: string;
15+
useDeleteProject?: typeof _useDeleteProject;
1816
};
1917

20-
export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({ projectName }) => {
18+
export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({
19+
projectName,
20+
useDeleteProject = _useDeleteProject,
21+
}) => {
2122
const popoverRef = useRef<MenuDomRef>(null);
2223
const [open, setOpen] = useState(false);
2324
const [dialogDeleteProjectIsOpen, setDialogDeleteProjectIsOpen] = useState(false);
2425
const { t } = useTranslation();
25-
const toast = useToast();
26-
const { trigger } = useApiResourceMutation<DeleteWorkspaceType>(DeleteProjectResource(projectName));
26+
27+
const { deleteProject } = useDeleteProject(projectName);
28+
2729
const handleOpenerClick = (e: Ui5CustomEvent<ButtonDomRef, ButtonClickEventDetail>) => {
2830
e.stopImmediatePropagation();
2931
e.stopPropagation();
@@ -59,10 +61,7 @@ export const ProjectsListItemMenu: FC<ProjectsListItemMenuProps> = ({ projectNam
5961
kubectl={<KubectlDeleteProject projectName={projectName} />}
6062
isOpen={dialogDeleteProjectIsOpen}
6163
setIsOpen={setDialogDeleteProjectIsOpen}
62-
onDeletionConfirmed={async () => {
63-
await trigger();
64-
toast.show(t('ProjectsListView.deleteConfirmationDialog'));
65-
}}
64+
onDeletionConfirmed={deleteProject}
6665
/>
6766
)}
6867
</div>

0 commit comments

Comments
 (0)