Skip to content

Commit 89f48a7

Browse files
test: create workspace
1 parent e05c36b commit 89f48a7

File tree

7 files changed

+473
-38
lines changed

7 files changed

+473
-38
lines changed

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 as _useDeleteWorkspace } from '../../../hooks/useDeleteWorkspace.ts';
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 = _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 } = useDeleteWorkspace(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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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: CreateWorkspaceParams | null = null;
7+
8+
const fakeUseCreateWorkspace: typeof useCreateWorkspace = () => ({
9+
createWorkspace: async (data: CreateWorkspaceParams): Promise<void> => {
10+
createWorkspacePayload = 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+
createWorkspacePayload = null;
23+
});
24+
25+
it('creates a workspace with valid data', () => {
26+
const setIsOpen = cy.stub();
27+
28+
cy.mount(
29+
<CreateWorkspaceDialogContainer
30+
useCreateWorkspace={fakeUseCreateWorkspace}
31+
useAuthOnboarding={fakeUseAuthOnboarding}
32+
isOpen={true}
33+
setIsOpen={setIsOpen}
34+
project="test-project"
35+
/>,
36+
);
37+
38+
const expectedPayload = {
39+
name: 'test-workspace',
40+
displayName: 'Test Workspace Display Name',
41+
chargingTarget: '12345678-1234-1234-1234-123456789abc',
42+
chargingTargetType: 'btp',
43+
members: [
44+
{
45+
name: 'name@domain.com',
46+
roles: ['admin'],
47+
kind: 'User',
48+
},
49+
],
50+
};
51+
52+
// Fill in the form (using Shadow DOM selectors)
53+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
54+
cy.get('#displayName').find('input[id*="inner"]').type('Test Workspace Display Name');
55+
56+
// Select charging target type
57+
cy.get('#chargingTargetType').click();
58+
cy.contains('BTP').click();
59+
60+
// Fill charging target
61+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
62+
63+
// Submit the form
64+
cy.get('ui5-button').contains('Create').click();
65+
66+
// Verify the hook was called with correct data
67+
cy.then(() => cy.wrap(createWorkspacePayload).deepEqualJson(expectedPayload));
68+
69+
// Dialog should close on success
70+
cy.wrap(setIsOpen).should('have.been.calledWith', false);
71+
});
72+
73+
it('validates required fields', () => {
74+
const setIsOpen = cy.stub();
75+
76+
cy.mount(
77+
<CreateWorkspaceDialogContainer
78+
useCreateWorkspace={fakeUseCreateWorkspace}
79+
useAuthOnboarding={fakeUseAuthOnboarding}
80+
isOpen={true}
81+
setIsOpen={setIsOpen}
82+
project="test-project"
83+
/>,
84+
);
85+
86+
// Try to submit without filling required fields
87+
cy.get('ui5-button').contains('Create').click();
88+
89+
// Should show validation errors - check for value-state="Negative" attribute
90+
cy.get('#name').should('have.attr', 'value-state', 'Negative');
91+
92+
// Or check if error message exists in DOM (even if hidden by CSS)
93+
cy.contains('This field is required').should('exist');
94+
95+
// Dialog should not close
96+
cy.wrap(setIsOpen).should('not.have.been.called');
97+
});
98+
99+
it('validates charging target format for BTP', () => {
100+
const setIsOpen = cy.stub();
101+
102+
cy.mount(
103+
<CreateWorkspaceDialogContainer
104+
useCreateWorkspace={fakeUseCreateWorkspace}
105+
useAuthOnboarding={fakeUseAuthOnboarding}
106+
isOpen={true}
107+
setIsOpen={setIsOpen}
108+
project="test-project"
109+
/>,
110+
);
111+
112+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
113+
cy.get('#chargingTargetType').click();
114+
cy.contains('BTP').click();
115+
116+
// Invalid format
117+
cy.get('#chargingTarget').find('input[id*="inner"]').type('invalid-format');
118+
cy.get('ui5-button').contains('Create').click();
119+
120+
// Should show validation error - check for value-state="Negative" attribute
121+
cy.get('#chargingTarget').should('have.attr', 'value-state', 'Negative');
122+
123+
// Dialog should not close
124+
cy.wrap(setIsOpen).should('not.have.been.called');
125+
});
126+
127+
it('should not close dialog when creation fails', () => {
128+
const failingUseCreateWorkspace: typeof useCreateWorkspace = () => ({
129+
createWorkspace: async (): Promise<void> => {
130+
throw new Error('Creation failed'); // Simulate failure by throwing error
131+
},
132+
isLoading: false,
133+
});
134+
135+
const setIsOpen = cy.stub();
136+
137+
cy.mount(
138+
<CreateWorkspaceDialogContainer
139+
useCreateWorkspace={failingUseCreateWorkspace}
140+
useAuthOnboarding={fakeUseAuthOnboarding}
141+
isOpen={true}
142+
setIsOpen={setIsOpen}
143+
project="test-project"
144+
/>,
145+
);
146+
147+
// Fill in the form
148+
cy.get('#name').find('input[id*="inner"]').type('test-workspace');
149+
cy.get('#chargingTargetType').click();
150+
cy.contains('BTP').click();
151+
cy.get('#chargingTarget').find('input[id*="inner"]').type('12345678-1234-1234-1234-123456789abc');
152+
153+
// Submit the form
154+
cy.get('ui5-button').contains('Create').click();
155+
156+
// Dialog should NOT close on failure
157+
cy.wrap(setIsOpen).should('not.have.been.called');
158+
159+
// Dialog should still be visible
160+
cy.contains('Create').should('be.visible');
161+
});
162+
});

src/components/Dialogs/CreateWorkspaceDialogContainer.tsx

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,16 @@
11
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';
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';
14-
import { useAuthOnboarding } from '../../spaces/onboarding/auth/AuthContextOnboarding.tsx';
4+
import { useAuthOnboarding as _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 as _useCreateWorkspace } from '../../hooks/useCreateWorkspace.ts';
12+
import { APIError } from '../../lib/api/error.ts';
13+
import { ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
2114

2215
export type CreateDialogProps = {
2316
name: string;
@@ -32,10 +25,14 @@ export function CreateWorkspaceDialogContainer({
3225
isOpen,
3326
setIsOpen,
3427
project = '',
28+
useCreateWorkspace = _useCreateWorkspace,
29+
useAuthOnboarding = _useAuthOnboarding,
3530
}: {
3631
isOpen: boolean;
3732
setIsOpen: (isOpen: boolean) => void;
3833
project?: string;
34+
useCreateWorkspace?: typeof _useCreateWorkspace;
35+
useAuthOnboarding?: typeof _useAuthOnboarding;
3936
}) {
4037
const { t } = useTranslation();
4138
const validationSchemaProjectWorkspace = useMemo(() => createProjectWorkspaceSchema(t), [t]);
@@ -59,6 +56,11 @@ export function CreateWorkspaceDialogContainer({
5956
const { user } = useAuthOnboarding();
6057

6158
const username = user?.email;
59+
const namespace = projectnameToNamespace(project);
60+
61+
const { createWorkspace } = useCreateWorkspace(project, namespace);
62+
const errorDialogRef = useRef<ErrorDialogHandle>(null);
63+
6264
const clearForm = useCallback(() => {
6365
resetField('name');
6466
resetField('chargingTarget');
@@ -74,30 +76,23 @@ export function CreateWorkspaceDialogContainer({
7476
clearForm();
7577
}
7678
}, [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);
8379

8480
const handleWorkspaceCreate = async ({
8581
name,
8682
displayName,
8783
chargingTarget,
84+
chargingTargetType,
8885
members,
8986
}: OnCreatePayload): Promise<boolean> => {
9087
try {
91-
await trigger(
92-
CreateWorkspace(name, namespace, {
93-
displayName: displayName,
94-
chargingTarget: chargingTarget,
95-
members: members,
96-
}),
97-
);
98-
await revalidate();
88+
await createWorkspace({
89+
name,
90+
displayName,
91+
chargingTarget,
92+
chargingTargetType,
93+
members,
94+
});
9995
setIsOpen(false);
100-
toast.show(t('CreateWorkspaceDialog.toastMessage'));
10196
return true;
10297
} catch (e) {
10398
console.error(e);

0 commit comments

Comments
 (0)