Skip to content

Commit ac6b028

Browse files
workspace tests for creation and deletion
1 parent 50559a8 commit ac6b028

File tree

8 files changed

+606
-103
lines changed

8 files changed

+606
-103
lines changed

package-lock.json

Lines changed: 98 additions & 54 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/ControlPlanes/List/ControlPlaneListWorkspaceGridTile.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,10 @@ import { ControlPlaneCard } from '../ControlPlaneCard/ControlPlaneCard.tsx';
77
import { ListWorkspacesType, isWorkspaceReady } from '../../../lib/api/types/crate/listWorkspaces.ts';
88
import { useMemo, useState } from 'react';
99
import { MembersAvatarView } from './MembersAvatarView.tsx';
10-
import { DeleteWorkspaceResource, DeleteWorkspaceType } from '../../../lib/api/types/crate/deleteWorkspace.ts';
11-
import { useApiResourceMutation, useApiResource } from '../../../lib/api/useApiResource.ts';
10+
import { useApiResource } from '../../../lib/api/useApiResource.ts';
1211
import { DISPLAY_NAME_ANNOTATION } from '../../../lib/api/types/shared/keyNames.ts';
1312
import { DeleteConfirmationDialog } from '../../Dialogs/DeleteConfirmationDialog.tsx';
1413
import { KubectlDeleteWorkspace } from '../../Dialogs/KubectlCommandInfo/Controllers/KubectlDeleteWorkspace.tsx';
15-
import { useToast } from '../../../context/ToastContext.tsx';
1614
import { ListControlPlanes } from '../../../lib/api/types/crate/controlPlanes.ts';
1715
import IllustratedError from '../../Shared/IllustratedError.tsx';
1816
import { APIError } from '../../../lib/api/error.ts';
@@ -24,13 +22,19 @@ import IllustrationMessageType from '@ui5/webcomponents-fiori/dist/types/Illustr
2422
import styles from './WorkspacesList.module.css';
2523
import { ControlPlanesListMenu } from '../ControlPlanesListMenu.tsx';
2624
import { CreateManagedControlPlaneWizardContainer } from '../../Wizards/CreateManagedControlPlane/CreateManagedControlPlaneWizardContainer.tsx';
25+
import { useDeleteWorkspace } from '../../../hooks/useDeleteWorkspace.tsx';
2726

2827
interface Props {
2928
projectName: string;
3029
workspace: ListWorkspacesType;
30+
useDeleteWorkspace?: typeof useDeleteWorkspace;
3131
}
3232

33-
export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Props) {
33+
export function ControlPlaneListWorkspaceGridTile({
34+
projectName,
35+
workspace,
36+
useDeleteWorkspace: useDeleteWorkspaceHook = useDeleteWorkspace,
37+
}: Props) {
3438
const [isCreateManagedControlPlaneWizardOpen, setIsCreateManagedControlPlaneWizardOpen] = useState(false);
3539
const [initialTemplateName, setInitialTemplateName] = useState<string | undefined>(undefined);
3640
const workspaceName = workspace.metadata.name;
@@ -40,13 +44,10 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
4044

4145
const { t } = useTranslation();
4246

43-
const toast = useToast();
4447
const [dialogDeleteWsIsOpen, setDialogDeleteWsIsOpen] = useState(false);
4548

4649
const { data: controlplanes, error: cpsError } = useApiResource(ListControlPlanes(projectName, workspaceName));
47-
const { trigger } = useApiResourceMutation<DeleteWorkspaceType>(
48-
DeleteWorkspaceResource(projectNamespace, workspaceName),
49-
);
50+
const { deleteWorkspace } = useDeleteWorkspaceHook(projectName, projectNamespace, workspaceName);
5051

5152
const { mcpCreationGuide } = useLink();
5253
const errorView = createErrorView(cpsError);
@@ -181,10 +182,7 @@ export function ControlPlaneListWorkspaceGridTile({ projectName, workspace }: Pr
181182
kubectl={<KubectlDeleteWorkspace projectName={projectName} resourceName={workspaceName} />}
182183
isOpen={dialogDeleteWsIsOpen}
183184
setIsOpen={setDialogDeleteWsIsOpen}
184-
onDeletionConfirmed={async () => {
185-
await trigger();
186-
toast.show(t('ControlPlaneListWorkspaceGridTile.deleteConfirmationDialog'));
187-
}}
185+
onDeletionConfirmed={deleteWorkspace}
188186
/>
189187
{isCreateManagedControlPlaneWizardOpen ? (
190188
<CreateManagedControlPlaneWizardContainer
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { CreateWorkspaceDialogContainer } from './CreateWorkspaceDialogContainer';
2+
import { useCreateWorkspace, CreateWorkspaceParams } from '../../hooks/useCreateWorkspace';
3+
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding';
4+
5+
describe('CreateWorkspaceDialogContainer', () => {
6+
let createWorkspacePayload: Omit<CreateWorkspaceParams, 'namespace'> | null = null;
7+
8+
const fakeUseCreateWorkspace: typeof useCreateWorkspace = () => ({
9+
createWorkspace: async (data: Omit<CreateWorkspaceParams, 'namespace'>): Promise<boolean> => {
10+
createWorkspacePayload = data;
11+
return true;
12+
},
13+
isLoading: false,
14+
errorDialogRef: { current: null },
15+
});
16+
17+
const fakeUseAuthOnboarding = (() => ({
18+
user: {
19+
20+
},
21+
})) as typeof useAuthOnboarding;
22+
23+
beforeEach(() => {
24+
createWorkspacePayload = null;
25+
});
26+
27+
it('creates a workspace with valid data', () => {
28+
const setIsOpen = cy.stub();
29+
30+
cy.mount(
31+
<CreateWorkspaceDialogContainer
32+
useCreateWorkspace={fakeUseCreateWorkspace}
33+
useAuthOnboarding={fakeUseAuthOnboarding}
34+
isOpen={true}
35+
setIsOpen={setIsOpen}
36+
project="test-project"
37+
/>,
38+
);
39+
40+
const expectedPayload = {
41+
name: 'test-workspace',
42+
displayName: 'Test Workspace Display Name',
43+
chargingTarget: '12345678-1234-1234-1234-123456789abc',
44+
members: [
45+
{
46+
47+
roles: ['admin'],
48+
kind: 'User',
49+
},
50+
],
51+
};
52+
53+
// Fill in the form (using Shadow DOM selectors)
54+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
55+
cy.get('#displayName').find('input[id*="inner"]').type('Test Workspace Display Name');
56+
57+
// Select charging target type
58+
cy.get('#chargingTargetType').click();
59+
cy.contains('BTP').click();
60+
61+
// Fill charging target
62+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
63+
64+
// Submit the form
65+
cy.get('ui5-button').contains('Create').click();
66+
67+
// Verify the hook was called with correct data
68+
cy.then(() => cy.wrap(createWorkspacePayload).deepEqualJson(expectedPayload));
69+
70+
// Dialog should close on success
71+
cy.wrap(setIsOpen).should('have.been.calledWith', false);
72+
});
73+
74+
it('validates required fields', () => {
75+
const setIsOpen = cy.stub();
76+
77+
cy.mount(
78+
<CreateWorkspaceDialogContainer
79+
useCreateWorkspace={fakeUseCreateWorkspace}
80+
useAuthOnboarding={fakeUseAuthOnboarding}
81+
isOpen={true}
82+
setIsOpen={setIsOpen}
83+
project="test-project"
84+
/>,
85+
);
86+
87+
// Try to submit without filling required fields
88+
cy.get('ui5-button').contains('Create').click();
89+
90+
// Should show validation errors - check for value-state="Negative" attribute
91+
cy.get('#name').should('have.attr', 'value-state', 'Negative');
92+
93+
// Or check if error message exists in DOM (even if hidden by CSS)
94+
cy.contains('This field is required').should('exist');
95+
96+
// Dialog should not close
97+
cy.wrap(setIsOpen).should('not.have.been.called');
98+
});
99+
100+
it('validates charging target format for BTP', () => {
101+
const setIsOpen = cy.stub();
102+
103+
cy.mount(
104+
<CreateWorkspaceDialogContainer
105+
useCreateWorkspace={fakeUseCreateWorkspace}
106+
useAuthOnboarding={fakeUseAuthOnboarding}
107+
isOpen={true}
108+
setIsOpen={setIsOpen}
109+
project="test-project"
110+
/>,
111+
);
112+
113+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
114+
cy.get('#chargingTargetType').click();
115+
cy.contains('BTP').click();
116+
117+
// Invalid format
118+
cy.get('#chargingTarget').find('input[id*="inner"]').type('invalid-format');
119+
cy.get('ui5-button').contains('Create').click();
120+
121+
// Should show validation error - check for value-state="Negative" attribute
122+
cy.get('#chargingTarget').should('have.attr', 'value-state', 'Negative');
123+
124+
// Dialog should not close
125+
cy.wrap(setIsOpen).should('not.have.been.called');
126+
});
127+
128+
it('should not close dialog when creation fails', () => {
129+
const failingUseCreateWorkspace: typeof useCreateWorkspace = () => ({
130+
createWorkspace: async (): Promise<boolean> => {
131+
return false; // Simulate failure
132+
},
133+
isLoading: false,
134+
errorDialogRef: { current: null },
135+
});
136+
137+
const setIsOpen = cy.stub();
138+
139+
cy.mount(
140+
<CreateWorkspaceDialogContainer
141+
useCreateWorkspace={failingUseCreateWorkspace}
142+
useAuthOnboarding={fakeUseAuthOnboarding}
143+
isOpen={true}
144+
setIsOpen={setIsOpen}
145+
project="test-project"
146+
/>,
147+
);
148+
149+
// Fill in the form
150+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
151+
cy.get('#chargingTargetType').click();
152+
cy.contains('BTP').click();
153+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
154+
155+
// Submit the form
156+
cy.get('ui5-button').contains('Create').click();
157+
158+
// Dialog should NOT close on failure
159+
cy.wrap(setIsOpen).should('not.have.been.called');
160+
161+
// Dialog should still be visible
162+
cy.contains('Create').should('be.visible');
163+
});
164+
});

src/components/Dialogs/CreateWorkspaceDialogContainer.tsx

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
1-
import { useCallback, useEffect, useMemo, useRef } from 'react';
2-
import { useApiResourceMutation, useRevalidateApiResource } from '../../lib/api/useApiResource';
3-
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
4-
import { APIError } from '../../lib/api/error';
1+
import { useCallback, useEffect, useMemo } from 'react';
52
import { CreateProjectWorkspaceDialog, OnCreatePayload } from './CreateProjectWorkspaceDialog.tsx';
6-
import {
7-
CreateWorkspace,
8-
CreateWorkspaceResource,
9-
CreateWorkspaceType,
10-
} from '../../lib/api/types/crate/createWorkspace';
113
import { projectnameToNamespace } from '../../utils';
12-
import { ListWorkspaces } from '../../lib/api/types/crate/listWorkspaces';
13-
import { useToast } from '../../context/ToastContext.tsx';
144
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
155
import { Member, MemberRoles } from '../../lib/api/types/shared/members.ts';
166
import { useTranslation } from 'react-i18next';
177
import { zodResolver } from '@hookform/resolvers/zod';
188
import { useForm } from 'react-hook-form';
199
import { createProjectWorkspaceSchema } from '../../lib/api/validations/schemas.ts';
2010
import { ComponentsListItem } from '../../lib/api/types/crate/createManagedControlPlane.ts';
11+
import { useCreateWorkspace } from '../../hooks/useCreateWorkspace.tsx';
2112

2213
export type CreateDialogProps = {
2314
name: string;
@@ -32,10 +23,14 @@ export function CreateWorkspaceDialogContainer({
3223
isOpen,
3324
setIsOpen,
3425
project = '',
26+
useCreateWorkspace: useCreateWorkspaceHook = useCreateWorkspace,
27+
useAuthOnboarding: useAuthOnboardingHook = useAuthOnboarding,
3528
}: {
3629
isOpen: boolean;
3730
setIsOpen: (isOpen: boolean) => void;
3831
project?: string;
32+
useCreateWorkspace?: typeof useCreateWorkspace;
33+
useAuthOnboarding?: typeof useAuthOnboarding;
3934
}) {
4035
const { t } = useTranslation();
4136
const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]);
@@ -56,9 +51,13 @@ export function CreateWorkspaceDialogContainer({
5651
chargingTargetType: '',
5752
},
5853
});
59-
const { user } = useAuthOnboarding();
54+
const { user } = useAuthOnboardingHook();
6055

6156
const username = user?.email;
57+
const namespace = projectnameToNamespace(project);
58+
59+
const { createWorkspace, errorDialogRef } = useCreateWorkspaceHook(project, namespace);
60+
6261
const clearForm = useCallback(() => {
6362
resetField('name');
6463
resetField('chargingTarget');
@@ -74,40 +73,25 @@ export function CreateWorkspaceDialogContainer({
7473
clearForm();
7574
}
7675
}, [resetField, setValue, username, isOpen, clearForm]);
77-
const namespace = projectnameToNamespace(project);
78-
const toast = useToast();
79-
80-
const { trigger } = useApiResourceMutation<CreateWorkspaceType>(CreateWorkspaceResource(namespace));
81-
const revalidate = useRevalidateApiResource(ListWorkspaces(project));
82-
const errorDialogRef = useRef<ErrorDialogHandle>(null);
8376

8477
const handleWorkspaceCreate = async ({
8578
name,
8679
displayName,
8780
chargingTarget,
8881
members,
8982
}: OnCreatePayload): Promise<boolean> => {
90-
try {
91-
await trigger(
92-
CreateWorkspace(name, namespace, {
93-
displayName: displayName,
94-
chargingTarget: chargingTarget,
95-
members: members,
96-
}),
97-
);
98-
await revalidate();
83+
const success = await createWorkspace({
84+
name,
85+
displayName,
86+
chargingTarget,
87+
members,
88+
});
89+
90+
if (success) {
9991
setIsOpen(false);
100-
toast.show(t('CreateWorkspaceDialog.toastMessage'));
101-
return true;
102-
} catch (e) {
103-
console.error(e);
104-
if (e instanceof APIError) {
105-
if (errorDialogRef.current) {
106-
errorDialogRef.current.showErrorDialog(`${e.message}: ${JSON.stringify(e.info)}`);
107-
}
108-
}
109-
return false;
11092
}
93+
94+
return success;
11195
};
11296

11397
return (

0 commit comments

Comments
 (0)