diff --git a/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/Workspaces.cy.ts b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/Workspaces.cy.ts new file mode 100644 index 0000000..2770733 --- /dev/null +++ b/workspaces/frontend/src/__tests__/cypress/cypress/tests/e2e/Workspaces.cy.ts @@ -0,0 +1,65 @@ +import { mockNamespaces } from '~/__mocks__/mockNamespaces'; +import { mockBFFResponse } from '~/__mocks__/utils'; + +describe('Workspaces Component', () => { + beforeEach(() => { + // Mock the namespaces API response + cy.intercept('GET', '/api/v1/namespaces', { + body: mockBFFResponse(mockNamespaces), + }).as('getNamespaces'); + cy.visit('/'); + cy.wait('@getNamespaces'); + }); + + function openDeleteModal() { + cy.findAllByTestId('table-body').first().findByTestId('action-column').click(); + cy.findByTestId('action-delete').click(); + cy.findByTestId('delete-modal-input').should('have.value', ''); + } + + it('should test the close mechanisms of the delete modal', () => { + const closeModalActions = [ + () => cy.get('button').contains('Cancel').click(), + () => cy.get('[aria-label="Close"]').click(), + ]; + + closeModalActions.forEach((closeAction) => { + openDeleteModal(); + cy.findByTestId('delete-modal-input').type('Some Text'); + cy.findByTestId('delete-modal').should('be.visible'); + closeAction(); + cy.findByTestId('delete-modal').should('not.exist'); + }); + + // Check that clicking outside the modal does not close it + openDeleteModal(); + cy.findByTestId('delete-modal').should('be.visible'); + cy.get('body').click(0, 0); + cy.findByTestId('delete-modal').should('be.visible'); + }); + + it('should verify the delete modal verification mechanism', () => { + openDeleteModal(); + cy.findByTestId('delete-modal').within(() => { + cy.get('strong') + .first() + .invoke('text') + .then((resourceName) => { + // Type incorrect resource name + cy.findByTestId('delete-modal-input').type('Wrong Name'); + cy.findByTestId('delete-modal-input').should('have.value', 'Wrong Name'); + cy.findByTestId('delete-modal-helper-text').should('be.visible'); + cy.get('button').contains('Delete').should('have.css', 'pointer-events', 'none'); + + // Clear and type correct resource name + cy.findByTestId('delete-modal-input').clear(); + cy.findByTestId('delete-modal-input').type(resourceName); + cy.findByTestId('delete-modal-input').should('have.value', resourceName); + cy.findByTestId('delete-modal-helper-text').should('not.be.exist'); + cy.get('button').contains('Delete').should('not.have.css', 'pointer-events', 'none'); + cy.get('button').contains('Delete').click(); + cy.findByTestId('delete-modal').should('not.exist'); + }); + }); + }); +}); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index d8b1e15..6312a52 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -27,6 +27,7 @@ import { useState } from 'react'; import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types'; import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails'; import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow'; +import DeleteModal from '~/shared/components/DeleteModal'; import Filter, { FilteredColumn } from 'shared/components/Filter'; import { formatRam } from 'shared/utilities/WorkspaceResources'; @@ -158,6 +159,7 @@ export const Workspaces: React.FunctionComponent = () => { const [workspaces, setWorkspaces] = useState(initialWorkspaces); const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState([]); const [selectedWorkspace, setSelectedWorkspace] = React.useState(null); + const [workspaceToDelete, setWorkspaceToDelete] = React.useState(null); const selectWorkspace = React.useCallback( (newSelectedWorkspace) => { @@ -288,6 +290,12 @@ export const Workspaces: React.FunctionComponent = () => { console.log(`Clicked on stop, on row ${workspace.name}`); }, []); + const handleDeleteClick = React.useCallback((workspace: Workspace) => { + const buttonElement = document.activeElement as HTMLElement; + buttonElement.blur(); // Remove focus from the currently focused button + setWorkspaceToDelete(workspace); // Open the modal and set workspace to delete + }, []); + const defaultActions = React.useCallback( (workspace: Workspace): IActions => [ @@ -301,7 +309,7 @@ export const Workspaces: React.FunctionComponent = () => { }, { title: 'Delete', - onClick: () => deleteAction(workspace), + onClick: () => handleDeleteClick(workspace), }, { isSeparator: true, @@ -315,7 +323,7 @@ export const Workspaces: React.FunctionComponent = () => { onClick: () => stopAction(workspace), }, ] as IActions, - [selectWorkspace, editAction, deleteAction, startRestartAction, stopAction], + [selectWorkspace, editAction, handleDeleteClick, startRestartAction, stopAction], ); // States @@ -360,7 +368,7 @@ export const Workspaces: React.FunctionComponent = () => { workspace={selectedWorkspace} onCloseClick={() => selectWorkspace(null)} onEditClick={() => editAction(selectedWorkspace)} - onDeleteClick={() => deleteAction(selectedWorkspace)} + onDeleteClick={() => handleDeleteClick(selectedWorkspace)} /> )} @@ -399,6 +407,7 @@ export const Workspaces: React.FunctionComponent = () => { id="workspaces-table-content" key={rowIndex} isExpanded={isWorkspaceExpanded(workspace)} + data-testid="table-body" > { 1 hour ago - - + + ({ + ...action, + 'data-testid': `action-${typeof action.title === 'string' ? action.title.toLowerCase() : ''}`, + }))} + /> {isWorkspaceExpanded(workspace) && ( @@ -439,6 +453,14 @@ export const Workspaces: React.FunctionComponent = () => { ))} + setWorkspaceToDelete(null)} + onDelete={() => workspaceToDelete && deleteAction(workspaceToDelete)} + /> void; + onDelete: (resourceName: string) => void; + title: string; +} + +const DeleteModal: React.FC = ({ + isOpen, + resourceName, + namespace, + title, + onClose, + onDelete, +}) => { + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + if (!isOpen) { + setInputValue(''); + } + }, [isOpen]); + + const handleDelete = () => { + if (inputValue === resourceName) { + onDelete(resourceName); + onClose(); + } else { + alert('Resource name does not match.'); + } + }; + + const handleInputChange = (event: React.FormEvent, value: string) => { + setInputValue(value); + }; + + const showWarning = inputValue !== '' && inputValue !== resourceName; + + return ( + + + + + + + Are you sure you want to delete {resourceName} in namespace{' '} + {namespace}? +
+
+ Please type the resource name to confirm: +
+ + {showWarning && ( + + } variant="error"> + The name doesn't match. Please enter exactly: {resourceName} + + + )} +
+
+
+ +
+ + +
+
+
+ ); +}; + +export default DeleteModal;