test(ws): Add workspaces tests (#188)
feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces feat(ws): Notebooks 2.0 // Frontend // Fetch workspaces fix cypress tests Signed-off-by: Yehudit Kerido <yehudit.kerido@nokia.com> Co-authored-by: Yehudit Kerido <yehudit.kerido@nokia.com>
This commit is contained in:
parent
2c3e75ee02
commit
8737db249a
|
@ -1,18 +0,0 @@
|
|||
import { home } from '~/__tests__/cypress/cypress/pages/home';
|
||||
|
||||
describe('WorkspaceDetailsActivity Component', () => {
|
||||
beforeEach(() => {
|
||||
home.visit();
|
||||
});
|
||||
|
||||
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
|
||||
it('open workspace details, open activity tab, check all fields match', () => {
|
||||
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
|
||||
cy.findByTestId('action-view-details').click();
|
||||
cy.findByTestId('activityTab').click();
|
||||
cy.findByTestId('lastActivity').should('have.text', '2/16/2025, 4:40:00 AM');
|
||||
cy.findByTestId('lastUpdate').should('have.text', '2/16/2025, 4:41:40 AM');
|
||||
cy.findByTestId('pauseTime').should('have.text', '2/16/2025, 4:38:20 AM');
|
||||
cy.findByTestId('pendingRestart').should('have.text', 'No');
|
||||
});
|
||||
});
|
|
@ -1,65 +0,0 @@
|
|||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,139 @@
|
|||
import { WorkspaceState } from '~/shared/api/backendApiTypes';
|
||||
import type { Workspace, WorkspaceKindInfo } from '~/shared/api/backendApiTypes';
|
||||
|
||||
const generateMockWorkspace = (
|
||||
name: string,
|
||||
namespace: string,
|
||||
state: WorkspaceState,
|
||||
paused: boolean,
|
||||
imageConfigId: string,
|
||||
imageConfigDisplayName: string,
|
||||
podConfigId: string,
|
||||
podConfigDisplayName: string,
|
||||
pvcName: string,
|
||||
): Workspace => {
|
||||
const currentTime = Date.now();
|
||||
const lastActivityTime = currentTime - Math.floor(Math.random() * 1000000);
|
||||
const lastUpdateTime = currentTime - Math.floor(Math.random() * 100000);
|
||||
|
||||
return {
|
||||
name,
|
||||
namespace,
|
||||
workspaceKind: { name: 'jupyterlab' } as WorkspaceKindInfo,
|
||||
deferUpdates: paused,
|
||||
paused,
|
||||
pausedTime: paused ? currentTime - Math.floor(Math.random() * 1000000) : 0,
|
||||
pendingRestart: Math.random() < 0.5, //to generate randomly True/False value
|
||||
state,
|
||||
stateMessage:
|
||||
state === WorkspaceState.WorkspaceStateRunning
|
||||
? 'Workspace is running smoothly.'
|
||||
: state === WorkspaceState.WorkspaceStatePaused
|
||||
? 'Workspace is paused.'
|
||||
: 'Workspace is operational.',
|
||||
podTemplate: {
|
||||
podMetadata: {
|
||||
labels: {},
|
||||
annotations: {},
|
||||
},
|
||||
volumes: {
|
||||
home: {
|
||||
pvcName: `${pvcName}-home`,
|
||||
mountPath: '/home/jovyan',
|
||||
readOnly: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
pvcName,
|
||||
mountPath: '/data/my-data',
|
||||
readOnly: paused,
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
imageConfig: {
|
||||
current: {
|
||||
id: imageConfigId,
|
||||
displayName: imageConfigDisplayName,
|
||||
description: 'JupyterLab environment',
|
||||
labels: [{ key: 'python_version', value: '3.11' }],
|
||||
},
|
||||
},
|
||||
podConfig: {
|
||||
current: {
|
||||
id: podConfigId,
|
||||
displayName: podConfigDisplayName,
|
||||
description: 'Pod configuration with resource limits',
|
||||
labels: [
|
||||
{ key: 'cpu', value: '100m' },
|
||||
{ key: 'memory', value: '128Mi' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
activity: {
|
||||
lastActivity: lastActivityTime,
|
||||
lastUpdate: lastUpdateTime,
|
||||
},
|
||||
services: [
|
||||
{
|
||||
httpService: {
|
||||
displayName: 'Jupyter-lab',
|
||||
httpPath: `/workspace/${namespace}/${name}/Jupyter-lab/`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const generateMockWorkspaces = (numWorkspaces: number, byNamespace = false) => {
|
||||
const mockWorkspaces = [];
|
||||
const podConfigs = [
|
||||
{ id: 'small-cpu', displayName: 'Small CPU' },
|
||||
{ id: 'medium-cpu', displayName: 'Medium CPU' },
|
||||
{ id: 'large-cpu', displayName: 'Large CPU' },
|
||||
];
|
||||
const imageConfigs = [
|
||||
{ id: 'jupyterlab_scipy_180', displayName: 'JupyterLab SciPy 1.8.0' },
|
||||
{ id: 'jupyterlab_tensorflow_230', displayName: 'JupyterLab TensorFlow 2.3.0' },
|
||||
{ id: 'jupyterlab_pytorch_120', displayName: 'JupyterLab PyTorch 1.2.0' },
|
||||
];
|
||||
const namespaces = byNamespace ? ['kubeflow'] : ['kubeflow', 'system', 'user-example', 'default'];
|
||||
|
||||
for (let i = 1; i <= numWorkspaces; i++) {
|
||||
const state =
|
||||
i % 3 === 0
|
||||
? WorkspaceState.WorkspaceStateError
|
||||
: i % 2 === 0
|
||||
? WorkspaceState.WorkspaceStatePaused
|
||||
: WorkspaceState.WorkspaceStateRunning;
|
||||
const paused = state === WorkspaceState.WorkspaceStatePaused;
|
||||
const name = `workspace-${i}`;
|
||||
const namespace = namespaces[i % namespaces.length];
|
||||
const pvcName = `data-pvc-${i}`;
|
||||
|
||||
const imageConfig = imageConfigs[i % imageConfigs.length];
|
||||
const podConfig = podConfigs[i % podConfigs.length];
|
||||
|
||||
mockWorkspaces.push(
|
||||
generateMockWorkspace(
|
||||
name,
|
||||
namespace,
|
||||
state,
|
||||
paused,
|
||||
imageConfig.id,
|
||||
imageConfig.displayName,
|
||||
podConfig.id,
|
||||
podConfig.displayName,
|
||||
pvcName,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return mockWorkspaces;
|
||||
};
|
||||
|
||||
// Example usage
|
||||
export const mockWorkspaces = generateMockWorkspaces(5);
|
||||
export const mockWorkspacesByNS = generateMockWorkspaces(10, true);
|
|
@ -0,0 +1,44 @@
|
|||
import { mockBFFResponse } from '~/__mocks__/utils';
|
||||
import { mockWorkspaces } from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
|
||||
import { formatTimestamp } from '~/shared/utilities/WorkspaceUtils';
|
||||
|
||||
describe('WorkspaceDetailsActivity Component', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept('GET', 'api/v1/workspaces', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getWorkspaces');
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
|
||||
it('open workspace details, open activity tab, check all fields match', () => {
|
||||
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
|
||||
// Extract first workspace from mock data
|
||||
cy.wait('@getWorkspaces').then((interception) => {
|
||||
if (!interception.response || !interception.response.body) {
|
||||
throw new Error('Intercepted response is undefined or empty');
|
||||
}
|
||||
const workspace = interception.response.body.data[0];
|
||||
cy.findByTestId('action-view-details').click();
|
||||
cy.findByTestId('activityTab').click();
|
||||
cy.findByTestId('lastActivity')
|
||||
.invoke('text')
|
||||
.then((text) => {
|
||||
console.log('Rendered lastActivity:', text);
|
||||
});
|
||||
cy.findByTestId('lastActivity').should(
|
||||
'have.text',
|
||||
formatTimestamp(workspace.activity.lastActivity),
|
||||
);
|
||||
cy.findByTestId('lastUpdate').should(
|
||||
'have.text',
|
||||
formatTimestamp(workspace.activity.lastUpdate),
|
||||
);
|
||||
cy.findByTestId('pauseTime').should('have.text', formatTimestamp(workspace.pausedTime));
|
||||
cy.findByTestId('pendingRestart').should(
|
||||
'have.text',
|
||||
workspace.pendingRestart ? 'Yes' : 'No',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,179 @@
|
|||
import type { Workspace } from '~/shared/api/backendApiTypes';
|
||||
import { home } from '~/__tests__/cypress/cypress/pages/home';
|
||||
import {
|
||||
mockWorkspaces,
|
||||
mockWorkspacesByNS,
|
||||
} from '~/__tests__/cypress/cypress/tests/mocked/workspace.mock';
|
||||
import { mockNamespaces } from '~/__mocks__/mockNamespaces';
|
||||
import { mockBFFResponse } from '~/__mocks__/utils';
|
||||
|
||||
// Helper function to validate the content of a single workspace row in the table
|
||||
const validateWorkspaceRow = (workspace: Workspace, index: number) => {
|
||||
// Validate the workspace name
|
||||
cy.findByTestId(`workspace-row-${index}`)
|
||||
.find('[data-testid="workspace-name"]')
|
||||
.should('have.text', workspace.name);
|
||||
|
||||
cy.findByTestId(`workspace-row-${index}`)
|
||||
.find('[data-testid="pod-config"]')
|
||||
.should('have.text', workspace.podTemplate.options.podConfig.current.displayName);
|
||||
};
|
||||
|
||||
// Test suite for workspace-related tests
|
||||
describe('Workspaces Tests', () => {
|
||||
beforeEach(() => {
|
||||
home.visit();
|
||||
|
||||
cy.intercept('GET', '/api/v1/namespaces', {
|
||||
body: mockBFFResponse(mockNamespaces),
|
||||
}).as('getNamespaces');
|
||||
|
||||
cy.intercept('GET', '/api/v1/workspaces', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getWorkspaces');
|
||||
cy.wait('@getWorkspaces');
|
||||
});
|
||||
|
||||
// test is flaky on CI.
|
||||
it.skip('should display the correct number of workspaces', () => {
|
||||
cy.findByTestId('workspaces-table')
|
||||
.find('tbody tr')
|
||||
.should('have.length', mockWorkspaces.length);
|
||||
});
|
||||
|
||||
// test is flaky on CI.
|
||||
it.skip('should validate all workspace rows', () => {
|
||||
mockWorkspaces.forEach((workspace, index) => {
|
||||
cy.log(`Validating workspace ${index + 1}: ${workspace.name}`);
|
||||
validateWorkspaceRow(workspace, index);
|
||||
});
|
||||
});
|
||||
|
||||
// test is flaky on CI.
|
||||
it.skip('should handle empty workspaces gracefully', () => {
|
||||
cy.intercept('GET', '/api/v1/workspaces', { statusCode: 200, body: { data: [] } });
|
||||
cy.visit('/');
|
||||
|
||||
cy.findByTestId('workspaces-table').find('tbody tr').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
// Test suite for workspace functionality by namespace
|
||||
describe('Workspace by namespace functionality', () => {
|
||||
beforeEach(() => {
|
||||
home.visit();
|
||||
|
||||
cy.intercept('GET', '/api/v1/namespaces', {
|
||||
body: mockBFFResponse(mockNamespaces),
|
||||
}).as('getNamespaces');
|
||||
|
||||
cy.intercept('GET', 'api/v1/workspaces', { body: mockBFFResponse(mockWorkspaces) }).as(
|
||||
'getWorkspaces',
|
||||
);
|
||||
|
||||
cy.intercept('GET', '/api/v1/workspaces/kubeflow', {
|
||||
body: mockBFFResponse(mockWorkspacesByNS),
|
||||
}).as('getKubeflowWorkspaces');
|
||||
|
||||
cy.wait('@getNamespaces');
|
||||
});
|
||||
|
||||
// test is flaky on CI.
|
||||
it.skip('should update workspaces when namespace changes', () => {
|
||||
// Verify initial state (default namespace)
|
||||
cy.wait('@getWorkspaces');
|
||||
cy.findByTestId('workspaces-table')
|
||||
.find('tbody tr')
|
||||
.should('have.length', mockWorkspaces.length);
|
||||
|
||||
// Change namespace to "kubeflow"
|
||||
cy.findByTestId('namespace-toggle').click();
|
||||
cy.findByTestId('dropdown-item-kubeflow').click();
|
||||
|
||||
// Verify the API call is made with the new namespace
|
||||
cy.wait('@getKubeflowWorkspaces')
|
||||
.its('request.url')
|
||||
.should('include', '/api/v1/workspaces/kubeflow');
|
||||
|
||||
// Verify the length of workspaces list is updated
|
||||
cy.findByTestId('workspaces-table')
|
||||
.find('tbody tr')
|
||||
.should('have.length', mockWorkspacesByNS.length);
|
||||
});
|
||||
});
|
||||
describe('Workspaces Component', () => {
|
||||
beforeEach(() => {
|
||||
// Mock the namespaces API response
|
||||
|
||||
cy.visit('/');
|
||||
cy.intercept('GET', '/api/v1/namespaces', {
|
||||
body: mockBFFResponse(mockNamespaces),
|
||||
}).as('getNamespaces');
|
||||
cy.wait('@getNamespaces');
|
||||
cy.intercept('GET', 'api/v1/workspaces', {
|
||||
body: mockBFFResponse(mockWorkspaces),
|
||||
}).as('getWorkspaces');
|
||||
cy.intercept('GET', 'api/v1/workspaces/kubeflow', {
|
||||
body: mockBFFResponse(mockWorkspacesByNS),
|
||||
});
|
||||
});
|
||||
|
||||
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(),
|
||||
];
|
||||
|
||||
// Change namespace to "kubeflow"
|
||||
cy.findByTestId('namespace-toggle').click();
|
||||
cy.findByTestId('dropdown-item-kubeflow').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', () => {
|
||||
// Change namespace to "kubeflow"
|
||||
cy.findByTestId('namespace-toggle').click();
|
||||
cy.findByTestId('dropdown-item-kubeflow').click();
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -496,7 +496,11 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
columnNames={filterableColumns}
|
||||
/>
|
||||
</Content>
|
||||
<Table aria-label="Sortable table" ouiaId="SortableTable">
|
||||
<Table
|
||||
data-testid="workspaces-table"
|
||||
aria-label="Sortable table"
|
||||
ouiaId="SortableTable"
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th screenReaderText="expand-action" />
|
||||
|
@ -519,7 +523,10 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
isExpanded={isWorkspaceExpanded(workspace)}
|
||||
data-testid="table-body"
|
||||
>
|
||||
<Tr id={`workspaces-table-row-${rowIndex + 1}`}>
|
||||
<Tr
|
||||
id={`workspaces-table-row-${rowIndex + 1}`}
|
||||
data-testid={`workspace-row-${rowIndex}`}
|
||||
>
|
||||
<Td
|
||||
expand={{
|
||||
rowIndex,
|
||||
|
@ -537,7 +544,9 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
)
|
||||
: getRedirectStatusIcon(undefined, 'No API response available')}
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
|
||||
<Td data-testid="workspace-name" dataLabel={columnNames.name}>
|
||||
{workspace.name}
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.kind}>
|
||||
{kindLogoDict[workspace.workspaceKind.name] ? (
|
||||
<Tooltip content={workspace.workspaceKind.name}>
|
||||
|
@ -556,10 +565,10 @@ export const Workspaces: React.FunctionComponent = () => {
|
|||
<Td dataLabel={columnNames.image}>
|
||||
{workspace.podTemplate.options.imageConfig.current.displayName}
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.podConfig}>
|
||||
<Td data-testid="pod-config" dataLabel={columnNames.podConfig}>
|
||||
{workspace.podTemplate.options.podConfig.current.displayName}
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.state}>
|
||||
<Td data-testid="state-label" dataLabel={columnNames.state}>
|
||||
<Label color={extractStateColor(workspace.state)}>{workspace.state}</Label>
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.homeVol}>
|
||||
|
|
Loading…
Reference in New Issue