Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(KFLUXUI-217): add contexts to show page #36

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/components/IntegrationTests/ContextsField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import { FormGroup } from '@patternfly/react-core';
import { Bullseye, FormGroup, Spinner } from '@patternfly/react-core';
import { FieldArray, useField, FieldArrayRenderProps } from 'formik';
import { getFieldId } from '../../../src/shared/components/formik-fields/field-utils';
import { useComponents } from '../../hooks/useComponents';
Expand Down Expand Up @@ -106,7 +106,9 @@ const ContextsField: React.FC<IntegrationTestContextProps> = ({ heading, fieldNa
)}
/>
) : (
'Loading Additional Component Context options'
<Bullseye>
<Spinner size="xl" />
</Bullseye>
)}
</FormGroup>
);
Expand Down
127 changes: 127 additions & 0 deletions src/components/IntegrationTests/EditContextsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as React from 'react';
import {
Alert,
AlertVariant,
Button,
ButtonType,
ButtonVariant,
ModalVariant,
Stack,
StackItem,
} from '@patternfly/react-core';
import { Formik, FormikValues } from 'formik';
import { k8sPatchResource } from '../../k8s/k8s-fetch';
import { IntegrationTestScenarioModel } from '../../models';
import { IntegrationTestScenarioKind, Context } from '../../types/coreBuildService';
import { ComponentProps, createModalLauncher } from '../modal/createModalLauncher';

Check warning on line 16 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L15-L16

Added lines #L15 - L16 were not covered by tests
import ContextsField from './ContextsField';
import { UnformattedContexts, formatContexts } from './IntegrationTestForm/utils/create-utils';

type EditContextsModalProps = ComponentProps & {
intTest: IntegrationTestScenarioKind;
};

export const EditContextsModal: React.FC<React.PropsWithChildren<EditContextsModalProps>> = ({
intTest,
onClose,
}) => {
const [error, setError] = React.useState<string>();

const getFormContextValues = (contexts: Context[] = []) => {
return contexts.map(({ name, description }) => ({ name, description }));
};

const updateIntegrationTest = async (values: FormikValues) => {
try {
await k8sPatchResource({
model: IntegrationTestScenarioModel,
queryOptions: {
name: intTest.metadata.name,
ns: intTest.metadata.namespace,
},
patches: [
{

Check warning on line 43 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L43

Added line #L43 was not covered by tests
op: 'replace',
path: '/spec/contexts',
value: formatContexts(values.contexts as UnformattedContexts),

Check warning on line 46 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L46

Added line #L46 was not covered by tests
},
],
});
onClose(null, { submitClicked: true });
} catch (e) {
const errMsg = e.message || e.toString();

Check warning on line 52 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L52

Added line #L52 was not covered by tests
setError(errMsg as string);
}
};

const onReset = () => {
onClose(null, { submitClicked: false });
};

const initialContexts = getFormContextValues(intTest?.spec?.contexts);

Check warning on line 62 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L62

Added line #L62 was not covered by tests
// When a user presses enter, make sure the form doesn't submit.
// Enter should be used to select values from the drop down,
// when using the keyboard, not submit the form.
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission on Enter key
}
};

return (
<Formik
onSubmit={updateIntegrationTest}
initialValues={{ contexts: initialContexts, confirm: false }}
onReset={onReset}
>
{({ handleSubmit, handleReset, isSubmitting, values }) => {
const isChanged = values.contexts !== initialContexts;
const showConfirmation = isChanged && values.strategy === 'Automatic';
const isValid = isChanged && (showConfirmation ? values.confirm : true);

return (
<div data-test={'edit-contexts-modal'} onKeyDown={handleKeyDown}>
<Stack hasGutter>
<StackItem>
<ContextsField fieldName="contexts" editing={true} />
</StackItem>
<StackItem>
{error && (
<Alert isInline variant={AlertVariant.danger} title="An error occurred">
{error}
</Alert>
)}
<Button
type={ButtonType.submit}
isLoading={isSubmitting}
onClick={(e) => {
e.preventDefault();
handleSubmit();
}}
isDisabled={!isValid || isSubmitting}
data-test={'update-contexts'}
>
Save
</Button>
<Button
variant={ButtonVariant.link}
onClick={handleReset}
data-test={'cancel-update-contexts'}
>
Cancel
</Button>
</StackItem>
</Stack>
</div>
);
}}
</Formik>
);
};

export const createEditContextsModal = createModalLauncher(EditContextsModal, {
'data-test': `edit-its-contexts`,
variant: ModalVariant.medium,
title: `Edit contexts`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import MetadataList from '../../../MetadataList';
import { useModalLauncher } from '../../../modal/ModalProvider';
import { useWorkspaceInfo } from '../../../Workspace/useWorkspaceInfo';
import { createEditContextsModal } from '../../EditContextsModal';
import { createEditParamsModal } from '../../EditParamsModal';
import { IntegrationTestLabels } from '../../IntegrationTestForm/types';
import {
Expand All @@ -46,6 +47,7 @@
const showModal = useModalLauncher();

const params = integrationTest?.spec?.params;
const contexts = integrationTest?.spec?.contexts;

Check warning on line 50 in src/components/IntegrationTests/IntegrationTestDetails/tabs/IntegrationTestOverviewTab.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/IntegrationTestDetails/tabs/IntegrationTestOverviewTab.tsx#L50

Added line #L50 was not covered by tests

return (
<>
Expand Down Expand Up @@ -142,6 +144,36 @@
})}
</>
)}
{contexts && (
<DescriptionListGroup data-test="its-overview-contexts">
<DescriptionListTerm>
Contexts{' '}
<Tooltip content="Contexts where the integration test can be applied.">
<OutlinedQuestionCircleIcon />
</Tooltip>
</DescriptionListTerm>
<DescriptionListDescription>
{pluralize(contexts.length, 'context')}
<div>
{' '}
<Button
variant={ButtonVariant.link}
className="pf-v5-u-pl-0"
onClick={() =>
showModal(
createEditContextsModal({
intTest: integrationTest,
}),
)
}
data-test="edit-context-button"
>
Edit contexts
</Button>
</div>
</DescriptionListDescription>
</DescriptionListGroup>
)}
{params && (
<DescriptionListGroup data-test="its-overview-params">
<DescriptionListTerm>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getLabelForParam,
getURLForParam,
formatParams,
formatContexts,
} from '../create-utils';

const createResourceMock = createK8sUtilMock('K8sQueryCreateResource');
Expand Down Expand Up @@ -241,3 +242,19 @@ describe('Create Utils formatParams', () => {
expect(formattedParams[0].value).toBe('val1');
});
});

describe('Create Utils formatContexts', () => {
it('Should render null if no contexts or empty array []', () => {
const formattedContexts = formatContexts([]);
expect(formattedContexts).toBeNull();
});

it('Should render 3 contexts ', () => {
const formattedContexts = formatContexts([
{ name: 'apple', description: 'an apple' },
{ name: 'mango', description: 'a mango' },
{ name: 'orange', description: 'an orange' },
]);
expect(formattedContexts.length).toBe(3);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export const formatParams = (params): Param[] => {
return newParams.length > 0 ? newParams : null;
};

export const formatContexts = (contexts = [], setDefault = false): Context[] | null => {
export type UnformattedContexts = { name: string; description: string }[];
export const formatContexts = (
contexts: UnformattedContexts = [],
setDefault: boolean = false,
): Context[] | null => {
const defaultContext = {
name: 'application',
description: 'execute the integration test in all cases - this would be the default state',
Expand Down
112 changes: 112 additions & 0 deletions src/components/IntegrationTests/__tests__/EditContextsModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useComponents } from '../../../hooks/useComponents';
import { k8sPatchResource } from '../../../k8s/k8s-fetch';
import { formikRenderer } from '../../../utils/test-utils';
import { EditContextsModal } from '../EditContextsModal';
import { IntegrationTestFormValues } from '../IntegrationTestForm/types';
import { MockIntegrationTests } from '../IntegrationTestsListView/__data__/mock-integration-tests';
import { contextOptions } from '../utils';

// Mock external dependencies
jest.mock('../../../k8s/k8s-fetch', () => ({
k8sPatchResource: jest.fn(),
}));
jest.mock('../../../hooks/useComponents', () => ({
useComponents: jest.fn(),
}));
jest.mock('../../Workspace/useWorkspaceInfo', () => ({
useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })),
}));

const useComponentsMock = useComponents as jest.Mock;
const patchResourceMock = k8sPatchResource as jest.Mock;
const onCloseMock = jest.fn();

const intTest = MockIntegrationTests[0];
const initialValues: IntegrationTestFormValues = {
name: intTest.metadata.name,
url: 'test-url',
optional: true,
contexts: intTest.spec.contexts,
};

const setup = () =>
formikRenderer(<EditContextsModal intTest={intTest} onClose={onCloseMock} />, initialValues);

beforeEach(() => {
jest.clearAllMocks();
useComponentsMock.mockReturnValue([[], true]);
});

describe('EditContextsModal', () => {
it('should render correct contexts', () => {
setup();
const contextOptionNames = contextOptions.map((ctx) => ctx.name);

screen.getByText('Contexts');
contextOptionNames.forEach((ctxName) => screen.queryByText(ctxName));
});

it('should show Save and Cancel buttons', () => {
setup();
// Save
screen.getByTestId('update-contexts');
// Cancel
screen.getByTestId('cancel-update-contexts');
});

it('should call onClose callback when cancel button is clicked', () => {
setup();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: false });
});

it('prevents form submission when pressing Enter', () => {
setup();
const form = screen.getByTestId('edit-contexts-modal');
fireEvent.keyDown(form, { key: 'Enter', code: 'Enter' });
expect(k8sPatchResource).not.toHaveBeenCalled();
});

it('calls updateIntegrationTest and onClose on form submission', async () => {
patchResourceMock.mockResolvedValue({});

setup();
const clearButton = screen.getByTestId('clear-button');
// Clear all selections
fireEvent.click(clearButton);
// Save button should now be active
fireEvent.click(screen.getByRole('button', { name: 'Save' }));

await waitFor(() => {
expect(patchResourceMock).toHaveBeenCalledTimes(1);
});

expect(patchResourceMock).toHaveBeenCalledWith(
expect.objectContaining({
queryOptions: { name: 'test-app-test-1', ns: 'test-namespace' },
patches: [{ op: 'replace', path: '/spec/contexts', value: null }],
}),
);
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: true });
});

it('displays an error message if k8sPatchResource fails', async () => {
patchResourceMock.mockRejectedValue('Failed to update contexts');
setup();

const clearButton = screen.getByTestId('clear-button');
// Clear all selections
fireEvent.click(clearButton);
// Click Save button
fireEvent.click(screen.getByRole('button', { name: 'Save' }));

// wait for the error message to appear
await waitFor(() => {
expect(patchResourceMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('An error occurred')).toBeInTheDocument();
expect(screen.queryByText('Failed to update contexts')).toBeInTheDocument();
});
});
});
Loading