diff --git a/kafka-ui-react-app/src/components/Topics/List/ListItem.tsx b/kafka-ui-react-app/src/components/Topics/List/ListItem.tsx deleted file mode 100644 index 0b72343c6b..0000000000 --- a/kafka-ui-react-app/src/components/Topics/List/ListItem.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React from 'react'; -import { - ClusterName, - TopicName, - TopicWithDetailedInfo, -} from 'redux/interfaces'; -import DropdownItem from 'components/common/Dropdown/DropdownItem'; -import Dropdown from 'components/common/Dropdown/Dropdown'; -import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal'; -import ClusterContext from 'components/contexts/ClusterContext'; -import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted'; -import { Tag } from 'components/common/Tag/Tag.styled'; -import VerticalElipsisIcon from 'components/common/Icons/VerticalElipsisIcon'; -import { TableKeyLink } from 'components/common/table/Table/TableKeyLink.styled'; - -import * as S from './List.styled'; - -export interface ListItemProps { - topic: TopicWithDetailedInfo; - selected: boolean; - toggleTopicSelected(topicName: TopicName): void; - deleteTopic: (clusterName: ClusterName, topicName: TopicName) => void; - recreateTopic: (clusterName: ClusterName, topicName: TopicName) => void; - clusterName: ClusterName; - clearTopicMessages(topicName: TopicName, clusterName: ClusterName): void; -} - -const ListItem: React.FC = ({ - topic: { - name, - internal, - partitions, - segmentSize, - replicationFactor, - cleanUpPolicy, - }, - selected, - toggleTopicSelected, - deleteTopic, - recreateTopic, - clusterName, - clearTopicMessages, -}) => { - const { isReadOnly, isTopicDeletionAllowed } = - React.useContext(ClusterContext); - - const [isDeleteTopicConfirmationVisible, setDeleteTopicConfirmationVisible] = - React.useState(false); - - const [ - isRecreateTopicConfirmationVisible, - setRecreateTopicConfirmationVisible, - ] = React.useState(false); - - const { outOfSyncReplicas, numberOfMessages } = React.useMemo(() => { - if (partitions === undefined || partitions.length === 0) { - return { - outOfSyncReplicas: 0, - numberOfMessages: 0, - }; - } - - return partitions.reduce( - (memo, { replicas, offsetMax, offsetMin }) => { - const outOfSync = replicas?.filter(({ inSync }) => !inSync); - return { - outOfSyncReplicas: memo.outOfSyncReplicas + (outOfSync?.length || 0), - numberOfMessages: memo.numberOfMessages + (offsetMax - offsetMin), - }; - }, - { - outOfSyncReplicas: 0, - numberOfMessages: 0, - } - ); - }, [partitions]); - - const deleteTopicHandler = React.useCallback(() => { - deleteTopic(clusterName, name); - }, [clusterName, deleteTopic, name]); - - const recreateTopicHandler = React.useCallback(() => { - recreateTopic(clusterName, name); - setRecreateTopicConfirmationVisible(false); - }, [recreateTopic, clusterName, name]); - - const clearTopicMessagesHandler = React.useCallback(() => { - clearTopicMessages(clusterName, name); - }, [clearTopicMessages, clusterName, name]); - const [vElipsisVisble, setVElipsisVisble] = React.useState(false); - - return ( - setVElipsisVisble(true)} - onMouseLeave={() => setVElipsisVisble(false)} - > - {!isReadOnly && ( - - {!internal && ( - { - toggleTopicSelected(name); - }} - /> - )} - - )} - - {internal && IN} - - {name} - - - {partitions?.length} - {outOfSyncReplicas} - {replicationFactor} - {numberOfMessages} - - - - - {!internal && !isReadOnly && vElipsisVisble ? ( -
- } right> - {cleanUpPolicy === 'DELETE' && ( - - Clear Messages - - )} - {isTopicDeletionAllowed && ( - setDeleteTopicConfirmationVisible(true)} - danger - > - Remove Topic - - )} - setRecreateTopicConfirmationVisible(true)} - danger - > - Recreate Topic - - -
- ) : null} - setDeleteTopicConfirmationVisible(false)} - onConfirm={deleteTopicHandler} - > - Are you sure want to remove {name} topic? - - setRecreateTopicConfirmationVisible(false)} - onConfirm={recreateTopicHandler} - > - Are you sure to recreate {name} topic? - - - - ); -}; - -export default ListItem; diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx index 039be9ad65..5dcc8a0443 100644 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/List.spec.tsx @@ -1,56 +1,45 @@ import React from 'react'; -import { mount, ReactWrapper } from 'enzyme'; -import { Route, Router } from 'react-router-dom'; -import { act } from 'react-dom/test-utils'; +import { render } from 'lib/testHelpers'; +import { screen, waitFor, within } from '@testing-library/react'; +import { Route, Router, StaticRouter } from 'react-router'; import ClusterContext, { ContextProps, } from 'components/contexts/ClusterContext'; import List, { TopicsListProps } from 'components/Topics/List/List'; import { createMemoryHistory } from 'history'; -import { StaticRouter } from 'react-router'; -import Search from 'components/common/Search/Search'; import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; -import { ConfirmationModalProps } from 'components/common/ConfirmationModal/ConfirmationModal'; -import theme from 'theme/theme'; -import { ThemeProvider } from 'styled-components'; -import { SortOrder } from 'generated-sources'; - -jest.mock( - 'components/common/ConfirmationModal/ConfirmationModal', - () => 'mock-ConfirmationModal' -); +import { CleanUpPolicy, SortOrder } from 'generated-sources'; +import userEvent from '@testing-library/user-event'; describe('List', () => { const setupComponent = (props: Partial = {}) => ( - - - + ); const historyMock = createMemoryHistory(); - const mountComponentWithProviders = ( + const renderComponentWithProviders = ( contextProps: Partial = {}, props: Partial = {}, history = historyMock ) => - mount( + render( { describe('when it has readonly flag', () => { it('does not render the Add a Topic button', () => { - const component = mountComponentWithProviders(); - expect(component.exists('Link')).toBeFalsy(); + renderComponentWithProviders(); + expect(screen.queryByText(/add a topic/)).not.toBeInTheDocument(); }); }); describe('when it does not have readonly flag', () => { - let fetchTopicsList = jest.fn(); - let component: ReactWrapper; - const internalTopicsSwitchName = 'input[name="ShowInternalTopics"]'; + const fetchTopicsList = jest.fn(); jest.useFakeTimers(); - beforeEach(() => { - fetchTopicsList = jest.fn(); - - component = mountComponentWithProviders( - { isReadOnly: false }, - { fetchTopicsList } - ); + afterEach(() => { + fetchTopicsList.mockClear(); }); it('renders the Add a Topic button', () => { - expect(component.exists('Link')).toBeTruthy(); + renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList }); + expect(screen.getByText(/add a topic/i)).toBeInTheDocument(); }); - it('calls setTopicsSearch on input', () => { + it('calls setTopicsSearch on input', async () => { const setTopicsSearch = jest.fn(); - component = mountComponentWithProviders({}, { setTopicsSearch }); + renderComponentWithProviders({}, { setTopicsSearch }); const query = 'topic'; - const input = component.find(Search); - input.props().handleSearch(query); - expect(setTopicsSearch).toHaveBeenCalledWith(query); + const searchElement = screen.getByPlaceholderText('Search by Topic Name'); + userEvent.type(searchElement, query); + await waitFor(() => { + expect(setTopicsSearch).toHaveBeenCalledWith(query); + }); }); it('show internal toggle state should be true if user has not used it yet', () => { - const toggle = component.find(internalTopicsSwitchName); - const { checked } = toggle.props(); + renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList }); + const internalCheckBox = screen.getByRole('checkbox'); - expect(checked).toEqual(true); + expect(internalCheckBox).toBeChecked(); }); it('show internal toggle state should match user preference', () => { localStorage.setItem('hideInternalTopics', 'true'); - component = mountComponentWithProviders( - { isReadOnly: false }, - { fetchTopicsList } - ); + renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList }); - const toggle = component.find(internalTopicsSwitchName); - const { checked } = toggle.props(); + const internalCheckBox = screen.getByRole('checkbox'); - expect(checked).toEqual(false); + expect(internalCheckBox).not.toBeChecked(); }); - it('should refetch topics on show internal toggle change', () => { - jest.clearAllMocks(); - const toggle = component.find(internalTopicsSwitchName); - const { checked } = toggle.props(); - toggle.simulate('change'); + it('should re-fetch topics on show internal toggle change', async () => { + renderComponentWithProviders({ isReadOnly: false }, { fetchTopicsList }); + const internalCheckBox: HTMLInputElement = screen.getByRole('checkbox'); - expect(fetchTopicsList).toHaveBeenLastCalledWith({ - search: '', - showInternal: !checked, - sortOrder: SortOrder.ASC, + userEvent.click(internalCheckBox); + const { value } = internalCheckBox; + + await waitFor(() => { + expect(fetchTopicsList).toHaveBeenLastCalledWith({ + search: '', + showInternal: value === 'on', + sortOrder: SortOrder.ASC, + }); }); }); it('should reset page query param on show internal toggle change', () => { const mockedHistory = createMemoryHistory(); jest.spyOn(mockedHistory, 'push'); - component = mountComponentWithProviders( + renderComponentWithProviders( { isReadOnly: false }, { fetchTopicsList }, mockedHistory ); - const toggle = component.find(internalTopicsSwitchName); - toggle.simulate('change'); + const internalCheckBox: HTMLInputElement = screen.getByRole('checkbox'); + userEvent.click(internalCheckBox); expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25'); }); @@ -153,127 +137,158 @@ describe('List', () => { it('should set cached page query param on show internal toggle change', async () => { const mockedHistory = createMemoryHistory(); jest.spyOn(mockedHistory, 'push'); - component = mountComponentWithProviders( + + const cachedPage = 5; + mockedHistory.push(`/?page=${cachedPage}&perPage=25`); + + renderComponentWithProviders( { isReadOnly: false }, { fetchTopicsList, totalPages: 10 }, mockedHistory ); - const cachedPage = 5; + const searchInput = screen.getByPlaceholderText('Search by Topic Name'); + userEvent.type(searchInput, 'nonEmptyString'); - mockedHistory.push(`/?page=${cachedPage}&perPage=25`); + await waitFor(() => { + expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25'); + }); - const input = component.find(Search); - input.props().handleSearch('nonEmptyString'); + userEvent.clear(searchInput); - expect(mockedHistory.push).toHaveBeenCalledWith('/?page=1&perPage=25'); - - input.props().handleSearch(''); - - expect(mockedHistory.push).toHaveBeenCalledWith( - `/?page=${cachedPage}&perPage=25` - ); + await waitFor(() => { + expect(mockedHistory.push).toHaveBeenCalledWith( + `/?page=${cachedPage}&perPage=25` + ); + }); }); }); describe('when some list items are selected', () => { const mockDeleteTopics = jest.fn(); + const mockDeleteTopic = jest.fn(); + const mockClearTopic = jest.fn(); const mockClearTopicsMessages = jest.fn(); + const mockRecreate = jest.fn(); + const fetchTopicsList = jest.fn(); + jest.useFakeTimers(); const pathname = '/ui/clusters/local/topics'; - const component = mount( - - - - {setupComponent({ - topics: [ - externalTopicPayload, - { ...externalTopicPayload, name: 'external.topic2' }, - ], - deleteTopics: mockDeleteTopics, - clearTopicsMessages: mockClearTopicsMessages, - })} - - - - ); - const getCheckboxInput = (at: number) => - component.find('TableRow').at(at).find('input[type="checkbox"]').at(0); - const getConfirmationModal = () => - component.find('mock-ConfirmationModal').at(0); - - it('renders delete/purge buttons', () => { - expect(getCheckboxInput(0).props().checked).toBeFalsy(); - expect(getCheckboxInput(1).props().checked).toBeFalsy(); - expect(component.find('.buttons').length).toEqual(0); - - // check first item - getCheckboxInput(0).simulate('change', { target: { checked: true } }); - expect(getCheckboxInput(0).props().checked).toBeTruthy(); - expect(getCheckboxInput(1).props().checked).toBeFalsy(); - - // check second item - getCheckboxInput(1).simulate('change', { target: { checked: true } }); - expect(getCheckboxInput(0).props().checked).toBeTruthy(); - expect(getCheckboxInput(1).props().checked).toBeTruthy(); - expect( - component.find('div[data-testid="delete-buttons"]').length - ).toEqual(1); - - // uncheck second item - getCheckboxInput(1).simulate('change', { target: { checked: false } }); - expect(getCheckboxInput(0).props().checked).toBeTruthy(); - expect(getCheckboxInput(1).props().checked).toBeFalsy(); - expect( - component.find('div[data-testid="delete-buttons"]').length - ).toEqual(1); - - // uncheck first item - getCheckboxInput(0).simulate('change', { target: { checked: false } }); - expect(getCheckboxInput(0).props().checked).toBeFalsy(); - expect(getCheckboxInput(1).props().checked).toBeFalsy(); - expect( - component.find('div[data-testid="delete-buttons"]').length - ).toEqual(0); + beforeEach(() => { + render( + + + + {setupComponent({ + topics: [ + { + ...externalTopicPayload, + cleanUpPolicy: CleanUpPolicy.DELETE, + }, + { ...externalTopicPayload, name: 'external.topic2' }, + ], + deleteTopics: mockDeleteTopics, + clearTopicsMessages: mockClearTopicsMessages, + recreateTopic: mockRecreate, + deleteTopic: mockDeleteTopic, + clearTopicMessages: mockClearTopic, + fetchTopicsList, + })} + + + + ); }); - const checkActionButtonClick = async (action: string) => { + afterEach(() => { + mockDeleteTopics.mockClear(); + mockClearTopicsMessages.mockClear(); + mockRecreate.mockClear(); + mockDeleteTopic.mockClear(); + }); + + const getCheckboxInput = (at: number) => { + const rows = screen.getAllByRole('row'); + return within(rows[at + 1]).getByRole('checkbox'); + }; + + it('renders delete/purge buttons', () => { + const firstCheckbox = getCheckboxInput(0); + const secondCheckbox = getCheckboxInput(1); + expect(firstCheckbox).not.toBeChecked(); + expect(secondCheckbox).not.toBeChecked(); + // expect(component.find('.buttons').length).toEqual(0); + + // check first item + userEvent.click(firstCheckbox); + expect(firstCheckbox).toBeChecked(); + expect(secondCheckbox).not.toBeChecked(); + + expect(screen.getByTestId('delete-buttons')).toBeInTheDocument(); + + // check second item + userEvent.click(secondCheckbox); + expect(firstCheckbox).toBeChecked(); + expect(secondCheckbox).toBeChecked(); + + expect(screen.getByTestId('delete-buttons')).toBeInTheDocument(); + + // uncheck second item + userEvent.click(secondCheckbox); + expect(firstCheckbox).toBeChecked(); + expect(secondCheckbox).not.toBeChecked(); + + expect(screen.getByTestId('delete-buttons')).toBeInTheDocument(); + + // uncheck first item + userEvent.click(firstCheckbox); + expect(firstCheckbox).not.toBeChecked(); + expect(secondCheckbox).not.toBeChecked(); + + expect(screen.queryByTestId('delete-buttons')).not.toBeInTheDocument(); + }); + + const checkActionButtonClick = async ( + action: 'deleteTopics' | 'clearTopicsMessages' + ) => { const buttonIndex = action === 'deleteTopics' ? 0 : 1; + const confirmationText = action === 'deleteTopics' ? 'Are you sure you want to remove selected topics?' : 'Are you sure you want to purge messages of selected topics?'; const mockFn = action === 'deleteTopics' ? mockDeleteTopics : mockClearTopicsMessages; - getCheckboxInput(0).simulate('change', { target: { checked: true } }); - getCheckboxInput(1).simulate('change', { target: { checked: true } }); - let modal = getConfirmationModal(); - expect(modal.prop('isOpen')).toBeFalsy(); - component - .find('div[data-testid="delete-buttons"]') - .find('button') - .at(buttonIndex) - .simulate('click'); - expect(modal.text()).toEqual(confirmationText); - modal = getConfirmationModal(); - expect(modal.prop('isOpen')).toBeTruthy(); - await act(async () => { - (modal.props() as ConfirmationModalProps).onConfirm(); + + const firstCheckbox = getCheckboxInput(0); + const secondCheckbox = getCheckboxInput(1); + userEvent.click(firstCheckbox); + userEvent.click(secondCheckbox); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + + const deleteButtonContainer = screen.getByTestId('delete-buttons'); + const buttonClickedElement = within(deleteButtonContainer).getAllByRole( + 'button' + )[buttonIndex]; + userEvent.click(buttonClickedElement); + + const modal = screen.getByRole('dialog'); + expect(within(modal).getByText(confirmationText)).toBeInTheDocument(); + userEvent.click(within(modal).getByRole('button', { name: 'Submit' })); + + await waitFor(() => { + expect(screen.queryByTestId('delete-buttons')).not.toBeInTheDocument(); }); - component.update(); - expect(getCheckboxInput(0).props().checked).toBeFalsy(); - expect(getCheckboxInput(1).props().checked).toBeFalsy(); - expect( - component.find('div[data-testid="delete-buttons"]').length - ).toEqual(0); + expect(mockFn).toBeCalledTimes(1); expect(mockFn).toBeCalledWith('local', [ externalTopicPayload.name, @@ -290,28 +305,84 @@ describe('List', () => { }); it('closes ConfirmationModal when clicked on the cancel button', async () => { - getCheckboxInput(0).simulate('change', { target: { checked: true } }); - getCheckboxInput(1).simulate('change', { target: { checked: true } }); - let modal = getConfirmationModal(); - expect(modal.prop('isOpen')).toBeFalsy(); - component - .find('div[data-testid="delete-buttons"]') - .find('button') - .at(0) - .simulate('click'); - modal = getConfirmationModal(); - expect(modal.prop('isOpen')).toBeTruthy(); - await act(async () => { - (modal.props() as ConfirmationModalProps).onCancel(); + const firstCheckbox = getCheckboxInput(0); + const secondCheckbox = getCheckboxInput(1); + + userEvent.click(firstCheckbox); + userEvent.click(secondCheckbox); + + const deleteButton = screen.getByText('Delete selected topics'); + + userEvent.click(deleteButton); + + const modal = screen.getByRole('dialog'); + userEvent.click(within(modal).getByText('Cancel')); + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(firstCheckbox).toBeChecked(); + expect(secondCheckbox).toBeChecked(); + + expect(screen.getByTestId('delete-buttons')).toBeInTheDocument(); + + expect(mockDeleteTopics).not.toHaveBeenCalled(); + }); + + const tableRowActionClickAndCheck = async ( + action: 'deleteTopics' | 'clearTopicsMessages' | 'recreate' + ) => { + const row = screen.getAllByRole('row')[1]; + userEvent.hover(row); + const actionBtn = within(row).getByRole('menu', { hidden: true }); + + userEvent.click(actionBtn); + + let textBtn; + let mock: jest.Mock; + + if (action === 'clearTopicsMessages') { + textBtn = 'Clear Messages'; + mock = mockClearTopic; + } else if (action === 'deleteTopics') { + textBtn = 'Remove Topic'; + mock = mockDeleteTopic; + } else { + textBtn = 'Recreate Topic'; + mock = mockRecreate; + } + + const ourAction = screen.getByText(textBtn); + + userEvent.click(ourAction); + + let dialog = screen.getByRole('dialog'); + expect(dialog).toBeInTheDocument(); + userEvent.click(within(dialog).getByRole('button', { name: 'Submit' })); + + await waitFor(() => { + expect(mock).toHaveBeenCalled(); + if (action === 'clearTopicsMessages') { + expect(fetchTopicsList).toHaveBeenCalled(); + } }); - component.update(); - expect(getConfirmationModal().prop('isOpen')).toBeFalsy(); - expect(getCheckboxInput(0).props().checked).toBeTruthy(); - expect(getCheckboxInput(1).props().checked).toBeTruthy(); - expect( - component.find('div[data-testid="delete-buttons"]').length - ).toEqual(1); - expect(mockDeleteTopics).toBeCalledTimes(0); + + userEvent.click(ourAction); + dialog = screen.getByRole('dialog'); + expect(dialog).toBeInTheDocument(); + userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + expect(mock).toHaveBeenCalledTimes(1); + }; + + it('should test the actions of the row and their modal and fetching for removing', async () => { + await tableRowActionClickAndCheck('deleteTopics'); + }); + + it('should test the actions of the row and their modal and fetching for clear', async () => { + await tableRowActionClickAndCheck('clearTopicsMessages'); + }); + + it('should test the actions of the row and their modal and fetching for recreate', async () => { + await tableRowActionClickAndCheck('recreate'); }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx deleted file mode 100644 index 5f30ef0866..0000000000 --- a/kafka-ui-react-app/src/components/Topics/List/__tests__/ListItem.spec.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { - externalTopicPayload, - internalTopicPayload, -} from 'redux/reducers/topics/__test__/fixtures'; -import ListItem, { ListItemProps } from 'components/Topics/List/ListItem'; -import { screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; - -const mockDelete = jest.fn(); -const clusterName = 'local'; -const mockDeleteMessages = jest.fn(); -const mockToggleTopicSelected = jest.fn(); -const mockRecreateTopic = jest.fn(); -jest.mock( - 'components/common/ConfirmationModal/ConfirmationModal', - () => 'mock-ConfirmationModal' -); - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: () => ['TOPIC_DELETION'], -})); - -describe('ListItem', () => { - const setupComponent = (props: Partial = {}) => ( - - - - -
- ); - - const getCheckbox = () => screen.getByRole('checkbox'); - - it('renders without checkbox for internal topic', () => { - render(setupComponent({ topic: internalTopicPayload })); - expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); - }); - - it('renders with checkbox for external topic', () => { - render(setupComponent({ topic: externalTopicPayload })); - - expect(getCheckbox()).toBeInTheDocument(); - }); - - it('triggers the toggleTopicSelected when clicked on the checkbox input', () => { - render(setupComponent({ topic: externalTopicPayload })); - expect(getCheckbox()).toBeInTheDocument(); - userEvent.click(getCheckbox()); - expect(mockToggleTopicSelected).toBeCalledTimes(1); - expect(mockToggleTopicSelected).toBeCalledWith(externalTopicPayload.name); - }); - - it('renders correct out of sync replicas number', () => { - render( - setupComponent({ - topic: { ...externalTopicPayload, partitions: undefined }, - }) - ); - - expect(screen.getAllByRole('cell', { name: '0' }).length).toBeTruthy(); - }); -}); diff --git a/kafka-ui-react-app/src/components/Topics/List/__tests__/TopicsTableCells.spec.tsx b/kafka-ui-react-app/src/components/Topics/List/__tests__/TopicsTableCells.spec.tsx new file mode 100644 index 0000000000..c30d57f76d --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/List/__tests__/TopicsTableCells.spec.tsx @@ -0,0 +1,149 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import { + MessagesCell, + OutOfSyncReplicasCell, + TitleCell, +} from 'components/Topics/List/TopicsTableCells'; +import { TableState } from 'lib/hooks/useTableState'; +import { screen } from '@testing-library/react'; +import { Topic } from 'generated-sources'; +import { topicsPayload } from 'redux/reducers/topics/__test__/fixtures'; + +describe('TopicsTableCells Components', () => { + const mockTableState: TableState = { + data: topicsPayload, + selectedIds: new Set([]), + idSelector: jest.fn(), + isRowSelectable: jest.fn(), + selectedCount: 0, + setRowsSelection: jest.fn(), + toggleSelection: jest.fn(), + }; + + describe('TitleCell Component', () => { + it('should check the TitleCell component Render without the internal option', () => { + const currentData = topicsPayload[1]; + render( + + ); + expect(screen.queryByText('IN')).not.toBeInTheDocument(); + expect(screen.getByText(currentData.name)).toBeInTheDocument(); + }); + + it('should check the TitleCell component Render without the internal option', () => { + const currentData = topicsPayload[0]; + render( + + ); + expect(screen.getByText('IN')).toBeInTheDocument(); + expect(screen.getByText(currentData.name)).toBeInTheDocument(); + }); + }); + + describe('OutOfSyncReplicasCell Component', () => { + it('should check the content of the OutOfSyncReplicasCell to return 0 if no partition is empty array', () => { + const currentData = topicsPayload[0]; + currentData.partitions = []; + render( + + ); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('should check the content of the OutOfSyncReplicasCell to return 0 if no partition is found', () => { + const currentData = topicsPayload[1]; + currentData.partitions = undefined; + render( + + ); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('should check the content of the OutOfSyncReplicasCell with the correct partition number', () => { + const currentData = topicsPayload[0]; + const partitionNumber = currentData.partitions?.reduce( + (memo, { replicas }) => { + const outOfSync = replicas?.filter(({ inSync }) => !inSync); + return memo + (outOfSync?.length || 0); + }, + 0 + ); + + render( + + ); + expect( + screen.getByText(partitionNumber ? partitionNumber.toString() : '0') + ).toBeInTheDocument(); + }); + }); + + describe('MessagesCell Component', () => { + it('should check the content of the MessagesCell to return 0 if no partition is empty array ', () => { + const currentData = topicsPayload[0]; + currentData.partitions = []; + render( + + ); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('should check the content of the MessagesCell to return 0 if no partition is found', () => { + const currentData = topicsPayload[0]; + currentData.partitions = undefined; + render( + + ); + expect(screen.getByText('0')).toBeInTheDocument(); + }); + + it('should check the content of the MessagesCell with the correct partition number', () => { + const currentData = topicsPayload[0]; + const partitionNumber = currentData.partitions?.reduce( + (memo, { offsetMax, offsetMin }) => { + return memo + (offsetMax - offsetMin); + }, + 0 + ); + render( + + ); + expect( + screen.getByText(partitionNumber ? partitionNumber.toString() : '0') + ).toBeInTheDocument(); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx b/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx index aad0120d02..c4e8d707e9 100644 --- a/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/New/__test__/New.spec.tsx @@ -122,4 +122,29 @@ describe('New', () => { expect(mockedHistory.push).toBeCalledTimes(1); expect(createTopicAPIPathMock.called()).toBeTruthy(); }); + + it('submits valid form that result in an error', async () => { + const createTopicAPIPathMock = fetchMock.postOnce( + createTopicAPIPath, + { throws: new Error('Something went wrong') }, + { + body: createTopicPayload, + } + ); + + const mocked = createMemoryHistory({ + initialEntries: [clusterTopicNewPath(clusterName)], + }); + + jest.spyOn(mocked, 'push'); + renderComponent(mocked); + + await waitFor(() => { + userEvent.type(screen.getByPlaceholderText('Topic Name'), topicName); + userEvent.click(screen.getByText(/submit/i)); + }); + + expect(createTopicAPIPathMock.called()).toBeTruthy(); + expect(mocked.push).toBeCalledTimes(0); + }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts index 4523cb2275..9977612a95 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/Filters.styled.ts @@ -257,7 +257,9 @@ export const ConfirmDeletionText = styled.h3` padding: 16px 0; `; -export const MessageLoading = styled.div` +export const MessageLoading = styled.div.attrs({ + role: 'contentLoader', +})` color: ${({ theme }) => theme.heading.h3.color}; font-size: ${({ theme }) => theme.heading.h3.fontSize}; display: ${(props) => (props.isLive ? 'flex' : 'none')}; diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.styled.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.styled.spec.tsx new file mode 100644 index 0000000000..3a1ddb4d8f --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Filters/__tests__/Filters.styled.spec.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import * as S from 'components/Topics/Topic/Details/Messages/Filters/Filters.styled'; +import { screen } from '@testing-library/react'; +import theme from 'theme/theme'; + +describe('Filters Styled components', () => { + describe('MessageLoading component', () => { + it('should check the styling during live', () => { + render(); + expect(screen.getByRole('contentLoader')).toHaveStyle({ + color: theme.heading.h3.color, + 'font-size': theme.heading.h3.fontSize, + display: 'flex', + }); + }); + + it('should check the styling during not live', () => { + render(); + expect(screen.getByRole('contentLoader', { hidden: true })).toHaveStyle({ + color: theme.heading.h3.color, + 'font-size': theme.heading.h3.fontSize, + display: 'none', + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx index a8ab3ae91c..265b1f4a72 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/Message.tsx @@ -20,7 +20,11 @@ const StyledDataCell = styled.td` min-width: 350px; `; -const Message: React.FC<{ message: TopicMessage }> = ({ +export interface Props { + message: TopicMessage; +} + +const Message: React.FC = ({ message: { timestamp, timestampType, diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Message.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Message.spec.tsx index a402e3b16e..fd52935d7f 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Message.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Message.spec.tsx @@ -1,29 +1,92 @@ import React from 'react'; -import { TopicMessageTimestampTypeEnum } from 'generated-sources'; -import Message from 'components/Topics/Topic/Details/Messages/Message'; +import { TopicMessage, TopicMessageTimestampTypeEnum } from 'generated-sources'; +import Message, { + Props, +} from 'components/Topics/Topic/Details/Messages/Message'; import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; +import dayjs from 'dayjs'; +import userEvent from '@testing-library/user-event'; + +const messageContentText = 'messageContentText'; + +jest.mock( + 'components/Topics/Topic/Details/Messages/MessageContent/MessageContent', + () => () => + ( + + {messageContentText} + + ) +); describe('Message component', () => { - it('shows the data in the table row', async () => { - render( + const mockMessage: TopicMessage = { + timestamp: new Date(), + timestampType: TopicMessageTimestampTypeEnum.CREATE_TIME, + offset: 0, + key: 'test-key', + partition: 6, + content: '{"data": "test"}', + headers: { header: 'test' }, + }; + + const renderComponent = ( + props: Partial = { + message: mockMessage, + } + ) => { + return render( - +
); - expect(screen.getByText('{"data": "test"}')).toBeInTheDocument(); - expect(screen.getByText('test-key')).toBeInTheDocument(); + }; + + it('shows the data in the table row', () => { + renderComponent(); + expect(screen.getByText(mockMessage.content as string)).toBeInTheDocument(); + expect(screen.getByText(mockMessage.key as string)).toBeInTheDocument(); + expect( + screen.getByText( + dayjs(mockMessage.timestamp).format('MM.DD.YYYY HH:mm:ss') + ) + ).toBeInTheDocument(); + expect(screen.getByText(mockMessage.offset.toString())).toBeInTheDocument(); + expect( + screen.getByText(mockMessage.partition.toString()) + ).toBeInTheDocument(); + }); + + it('check the useDataSaver functionality', () => { + const props = { message: { ...mockMessage } }; + delete props.message.content; + renderComponent(props); + expect( + screen.queryByText(mockMessage.content as string) + ).not.toBeInTheDocument(); + }); + + it('should check the dropdown being visible during hover', () => { + renderComponent(); + const text = 'Save as a file'; + const trElement = screen.getByRole('row'); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + + userEvent.hover(trElement); + expect(screen.getByText(text)).toBeInTheDocument(); + + userEvent.unhover(trElement); + expect(screen.queryByText(text)).not.toBeInTheDocument(); + }); + + it('should check open Message Content functionality', () => { + renderComponent(); + const messageToggleIcon = screen.getByRole('button', { hidden: true }); + expect(screen.queryByText(messageContentText)).not.toBeInTheDocument(); + userEvent.click(messageToggleIcon); + expect(screen.getByText(messageContentText)).toBeInTheDocument(); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx index d2a92c09e3..b752f901d4 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/Messages.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { screen } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { render, EventSourceMock } from 'lib/testHelpers'; import Messages, { SeekDirectionOptions, @@ -40,7 +40,7 @@ describe('Messages', () => { ); }); - it('should check the SeekDirection select changes', () => { + it('should check the SeekDirection select changes with live option', async () => { const seekDirectionSelect = screen.getAllByRole('listbox')[1]; const seekDirectionOption = screen.getAllByRole('option')[1]; @@ -50,17 +50,28 @@ describe('Messages', () => { const labelValue1 = SeekDirectionOptions[1].label; userEvent.click(seekDirectionSelect); - userEvent.selectOptions(seekDirectionSelect, [ - SeekDirectionOptions[1].label, - ]); + userEvent.selectOptions(seekDirectionSelect, [labelValue1]); expect(seekDirectionOption).toHaveTextContent(labelValue1); const labelValue0 = SeekDirectionOptions[0].label; userEvent.click(seekDirectionSelect); - userEvent.selectOptions(seekDirectionSelect, [ - SeekDirectionOptions[0].label, - ]); + userEvent.selectOptions(seekDirectionSelect, [labelValue0]); expect(seekDirectionOption).toHaveTextContent(labelValue0); + + const liveOptionConf = SeekDirectionOptions[2]; + const labelValue2 = liveOptionConf.label; + userEvent.click(seekDirectionSelect); + const liveModeLi = screen.getByRole( + (role, element) => + role === 'option' && + element?.getAttribute('value') === liveOptionConf.value + ); + userEvent.selectOptions(seekDirectionSelect, [liveModeLi]); + expect(seekDirectionOption).toHaveTextContent(labelValue2); + + await waitFor(() => { + expect(screen.getByRole('contentLoader')).toBeInTheDocument(); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx index 3994e1e1e4..8e588a9efa 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Messages/__test__/MessagesTable.spec.tsx @@ -3,17 +3,23 @@ import { screen } from '@testing-library/react'; import { render } from 'lib/testHelpers'; import MessagesTable from 'components/Topics/Topic/Details/Messages/MessagesTable'; import { Router } from 'react-router'; -import { createMemoryHistory } from 'history'; -import { SeekDirection, SeekType } from 'generated-sources'; +import { createMemoryHistory, MemoryHistory } from 'history'; +import { SeekDirection, SeekType, TopicMessage } from 'generated-sources'; import userEvent from '@testing-library/user-event'; import TopicMessagesContext, { ContextProps, } from 'components/contexts/TopicMessagesContext'; +import { + topicMessagePayload, + topicMessagesMetaPayload, +} from 'redux/reducers/topicMessages/__test__/fixtures'; + +const mockTopicsMessages: TopicMessage[] = [{ ...topicMessagePayload }]; describe('MessagesTable', () => { - const searchParams = new URLSearchParams( - `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9` - ); + const seekToResult = '&seekTo=0::9'; + const searchParamsValue = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}${seekToResult}`; + const searchParams = new URLSearchParams(searchParamsValue); const contextValue: ContextProps = { isLive: false, seekDirection: SeekDirection.FORWARD, @@ -23,18 +29,32 @@ describe('MessagesTable', () => { const setUpComponent = ( params: URLSearchParams = searchParams, - ctx: ContextProps = contextValue + ctx: ContextProps = contextValue, + messages: TopicMessage[] = [], + customHistory?: MemoryHistory ) => { - const history = createMemoryHistory(); - history.push({ - search: params.toString(), - }); + const history = + customHistory || + createMemoryHistory({ + initialEntries: [params.toString()], + }); return render( - +
, + { + preloadedState: { + topicMessages: { + messages, + meta: { + ...topicMessagesMetaPayload, + }, + isFetching: false, + }, + }, + } ); }; @@ -69,5 +89,55 @@ describe('MessagesTable', () => { setUpComponent(searchParams, { ...contextValue, isLive: true }); expect(screen.getByRole('progressbar')).toBeInTheDocument(); }); + + it('should check the seekTo parameter in the url if no seekTo is found should noy change the history', () => { + const customSearchParam = new URLSearchParams(searchParamsValue); + + const mockedHistory = createMemoryHistory({ + initialEntries: [customSearchParam.toString()], + }); + jest.spyOn(mockedHistory, 'push'); + + setUpComponent(customSearchParam, contextValue, [], mockedHistory); + + userEvent.click(screen.getByRole('button', { name: 'Next' })); + expect(mockedHistory.push).toHaveBeenCalledWith({ + search: searchParamsValue.replace(seekToResult, '&seekTo=0%3A%3A1'), + }); + }); + + it('should check the seekTo parameter in the url if no seekTo is found should change the history', () => { + const customSearchParam = new URLSearchParams( + searchParamsValue.replace(seekToResult, '') + ); + + const mockedHistory = createMemoryHistory({ + initialEntries: [customSearchParam.toString()], + }); + jest.spyOn(mockedHistory, 'push'); + + setUpComponent( + customSearchParam, + { ...contextValue, searchParams: customSearchParam }, + [], + mockedHistory + ); + + userEvent.click(screen.getByRole('button', { name: 'Next' })); + expect(mockedHistory.push).not.toHaveBeenCalled(); + }); + }); + + describe('should render Messages table with data', () => { + beforeEach(() => { + setUpComponent(searchParams, { ...contextValue }, mockTopicsMessages); + }); + + it('should check the rendering of the messages', () => { + expect(screen.queryByText(/No messages found/i)).not.toBeInTheDocument(); + expect( + screen.getByText(mockTopicsMessages[0].content as string) + ).toBeInTheDocument(); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx index b81b5209f0..daa1ef7010 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Overview/__test__/Overview.spec.tsx @@ -49,8 +49,8 @@ describe('Overview', () => { underReplicatedPartitions?: number, inSyncReplicas?: number, replicas?: number - ) => - render( + ) => { + return render( { /> ); + }; + + afterEach(() => { + mockClearTopicMessages.mockClear(); + }); describe('when it has internal flag', () => { it('does not render the Action button a Topic', () => { @@ -116,4 +121,49 @@ describe('Overview', () => { expect(mockClearTopicMessages).toHaveBeenCalledTimes(1); }); + + describe('when the table partition dropdown appearance', () => { + it('should check if the dropdown is not present when it is readOnly', () => { + setupComponent( + { + ...defaultProps, + partitions: mockPartitions, + internal: true, + cleanUpPolicy: CleanUpPolicy.DELETE, + }, + { ...defaultContextValues, isReadOnly: true } + ); + expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); + }); + + it('should check if the dropdown is not present when it is internal', () => { + setupComponent({ + ...defaultProps, + partitions: mockPartitions, + internal: true, + cleanUpPolicy: CleanUpPolicy.DELETE, + }); + expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); + }); + + it('should check if the dropdown is not present when cleanUpPolicy is not DELETE', () => { + setupComponent({ + ...defaultProps, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.COMPACT, + }); + expect(screen.queryByText('Clear Messages')).not.toBeInTheDocument(); + }); + + it('should check if the dropdown action to be in visible', () => { + setupComponent({ + ...defaultProps, + partitions: mockPartitions, + internal: false, + cleanUpPolicy: CleanUpPolicy.DELETE, + }); + expect(screen.getByText('Clear Messages')).toBeInTheDocument(); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.styled.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.styled.spec.tsx new file mode 100644 index 0000000000..01c5981e43 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Details/Settings/__test__/Settings.styled.spec.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from 'lib/testHelpers'; +import { ConfigItemCell } from 'components/Topics/Topic/Details/Settings/Settings.styled'; + +describe('Settings styled Components', () => { + describe('ConfigItemCell Component', () => { + const renderComponent = ( + props: Partial<{ $hasCustomValue: boolean }> = {} + ) => { + return render( + + + + + + +
+ ); + }; + it('should check the true rendering ConfigItemList', () => { + renderComponent({ $hasCustomValue: true }); + expect(screen.getByRole('cell')).toHaveStyleRule( + 'font-weight', + '500 !important' + ); + }); + + it('should check the true rendering ConfigItemList', () => { + renderComponent(); + expect(screen.getByRole('cell')).toHaveStyleRule( + 'font-weight', + '400 !important' + ); + }); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx new file mode 100644 index 0000000000..ef3f91d459 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/DangerZone/__test__/DangerZone.spec.tsx @@ -0,0 +1,287 @@ +import React from 'react'; +import DangerZone, { + Props, +} from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; +import { screen, waitFor, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from 'lib/testHelpers'; +import { + topicName, + clusterName, +} from 'components/Topics/Topic/Edit/__test__/fixtures'; + +const defaultPartitions = 3; +const defaultReplicationFactor = 3; + +const renderComponent = (props?: Partial) => { + return render( + + ); +}; + +const clickOnDialogSubmitButton = () => { + userEvent.click( + within(screen.getByRole('dialog')).getByRole('button', { + name: 'Submit', + }) + ); +}; + +const checkDialogThenPressCancel = async () => { + const dialog = screen.getByRole('dialog'); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + + await waitFor(() => { + userEvent.click(within(dialog).getByText(/cancel/i)); + }); + + await waitFor(() => + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + ); +}; + +describe('DangerZone', () => { + it('renders the component', () => { + renderComponent(); + + const numberOfPartitionsEditForm = screen.getByRole('form', { + name: 'Edit number of partitions', + }); + expect(numberOfPartitionsEditForm).toBeInTheDocument(); + expect( + within(numberOfPartitionsEditForm).getByRole('spinbutton', { + name: 'Number of partitions *', + }) + ).toBeInTheDocument(); + expect( + within(numberOfPartitionsEditForm).getByRole('button', { name: 'Submit' }) + ).toBeInTheDocument(); + + const replicationFactorEditForm = screen.getByRole('form', { + name: 'Edit replication factor', + }); + expect(replicationFactorEditForm).toBeInTheDocument(); + expect( + within(replicationFactorEditForm).getByRole('spinbutton', { + name: 'Replication Factor *', + }) + ).toBeInTheDocument(); + expect( + within(replicationFactorEditForm).getByRole('button', { name: 'Submit' }) + ).toBeInTheDocument(); + }); + + it('calls updateTopicPartitionsCount', async () => { + const mockUpdateTopicPartitionsCount = jest.fn(); + renderComponent({ + updateTopicPartitionsCount: mockUpdateTopicPartitionsCount, + }); + const numberOfPartitionsEditForm = screen.getByRole('form', { + name: 'Edit number of partitions', + }); + + userEvent.type( + within(numberOfPartitionsEditForm).getByRole('spinbutton'), + '4' + ); + userEvent.click(within(numberOfPartitionsEditForm).getByRole('button')); + + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await waitFor(() => clickOnDialogSubmitButton()); + + expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1); + }); + + it('calls updateTopicReplicationFactor', async () => { + const mockUpdateTopicReplicationFactor = jest.fn(); + renderComponent({ + updateTopicReplicationFactor: mockUpdateTopicReplicationFactor, + }); + + const replicationFactorEditForm = screen.getByRole('form', { + name: 'Edit replication factor', + }); + expect( + within(replicationFactorEditForm).getByRole('spinbutton', { + name: 'Replication Factor *', + }) + ).toBeInTheDocument(); + expect( + within(replicationFactorEditForm).getByRole('button', { name: 'Submit' }) + ).toBeInTheDocument(); + + userEvent.type( + within(replicationFactorEditForm).getByRole('spinbutton'), + '4' + ); + userEvent.click(within(replicationFactorEditForm).getByRole('button')); + + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + await waitFor(() => clickOnDialogSubmitButton()); + + await waitFor(() => { + expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1); + }); + }); + + it('should view the validation error when partition value is lower than the default passed or empty', async () => { + renderComponent(); + const partitionInput = screen.getByPlaceholderText('Number of partitions'); + const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; + const value = (defaultPartitions - 4).toString(); + + expect(partitionInputSubmitBtn).toBeDisabled(); + await waitFor(() => { + userEvent.clear(partitionInput); + userEvent.type(partitionInput, value); + }); + + expect(partitionInput).toHaveValue(+value); + expect(partitionInputSubmitBtn).toBeEnabled(); + userEvent.click(partitionInputSubmitBtn); + + await waitFor(() => { + expect( + screen.getByText(/You can only increase the number of partitions!/i) + ).toBeInTheDocument(); + }); + + await waitFor(() => { + userEvent.clear(partitionInput); + }); + expect(screen.getByText(/are required/i)).toBeInTheDocument(); + }); + + it('should view the validation error when Replication Facto value is lower than the default passed or empty', async () => { + renderComponent(); + const replicatorFactorInput = + screen.getByPlaceholderText('Replication Factor'); + const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; + + await waitFor(() => { + userEvent.clear(replicatorFactorInput); + }); + + expect(replicatorFactorInputSubmitBtn).toBeEnabled(); + await waitFor(() => { + userEvent.click(replicatorFactorInputSubmitBtn); + }); + + expect(screen.getByText(/are required/i)).toBeInTheDocument(); + + await waitFor(() => { + userEvent.type(replicatorFactorInput, '1'); + }); + expect(screen.queryByText(/are required/i)).not.toBeInTheDocument(); + }); + + it('should close any popup if the partitionsCount is Increased ', () => { + renderComponent({ partitionsCountIncreased: true }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should close any popup if the replicationFactor is Updated', () => { + renderComponent({ replicationFactorUpdated: true }); + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + + it('should already opened Confirmation popup if partitionsCount is Increased', async () => { + const { rerender } = renderComponent(); + const partitionInput = screen.getByPlaceholderText('Number of partitions'); + const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; + + await waitFor(() => { + userEvent.type(partitionInput, '5'); + }); + + userEvent.click(partitionInputSubmitBtn); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + rerender( + + ); + await waitFor(() => + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + ); + }); + + it('should already opened Confirmation popup if replicationFactor is Increased', async () => { + const { rerender } = renderComponent(); + const replicatorFactorInput = + screen.getByPlaceholderText('Replication Factor'); + const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; + + await waitFor(() => { + userEvent.type(replicatorFactorInput, '5'); + }); + + userEvent.click(replicatorFactorInputSubmitBtn); + await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); + rerender( + + ); + await waitFor(() => + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + ); + }); + + it('should close the partitions dialog if he cancel button is pressed', async () => { + renderComponent(); + const partitionInput = screen.getByPlaceholderText('Number of partitions'); + const partitionInputSubmitBtn = screen.getAllByText(/submit/i)[0]; + + await waitFor(() => { + userEvent.type(partitionInput, '5'); + }); + + await waitFor(() => { + userEvent.click(partitionInputSubmitBtn); + }); + + await checkDialogThenPressCancel(); + }); + + it('should close the replicator dialog if he cancel button is pressed', async () => { + renderComponent(); + const replicatorFactorInput = + screen.getByPlaceholderText('Replication Factor'); + const replicatorFactorInputSubmitBtn = screen.getAllByText(/submit/i)[1]; + + await waitFor(() => { + userEvent.type(replicatorFactorInput, '5'); + }); + + await waitFor(() => { + userEvent.click(replicatorFactorInputSubmitBtn); + }); + + await checkDialogThenPressCancel(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx index 33f597e0c9..7ec2d4ab10 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/Edit.tsx @@ -46,7 +46,7 @@ const EditWrapperStyled = styled.div` } `; -const DEFAULTS = { +export const DEFAULTS = { partitions: 1, replicationFactor: 1, minInSyncReplicas: 1, diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/DangerZone.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/DangerZone.spec.tsx deleted file mode 100644 index 0b71065e3c..0000000000 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/DangerZone.spec.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import DangerZone, { - Props, -} from 'components/Topics/Topic/Edit/DangerZone/DangerZone'; -import { screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { render } from 'lib/testHelpers'; - -import { topicName, clusterName } from './fixtures'; - -const renderComponent = (props?: Partial) => - render( - - ); - -const clickOnDialogSubmitButton = () => { - userEvent.click( - within(screen.getByRole('dialog')).getByRole('button', { - name: 'Submit', - }) - ); -}; -describe('DangerZone', () => { - it('renders', () => { - renderComponent(); - - const numberOfPartitionsEditForm = screen.getByRole('form', { - name: 'Edit number of partitions', - }); - expect(numberOfPartitionsEditForm).toBeInTheDocument(); - expect( - within(numberOfPartitionsEditForm).getByRole('spinbutton', { - name: 'Number of partitions *', - }) - ).toBeInTheDocument(); - expect( - within(numberOfPartitionsEditForm).getByRole('button', { name: 'Submit' }) - ).toBeInTheDocument(); - - const replicationFactorEditForm = screen.getByRole('form', { - name: 'Edit replication factor', - }); - expect(replicationFactorEditForm).toBeInTheDocument(); - expect( - within(replicationFactorEditForm).getByRole('spinbutton', { - name: 'Replication Factor *', - }) - ).toBeInTheDocument(); - expect( - within(replicationFactorEditForm).getByRole('button', { name: 'Submit' }) - ).toBeInTheDocument(); - }); - - it('calls updateTopicPartitionsCount', async () => { - const mockUpdateTopicPartitionsCount = jest.fn(); - renderComponent({ - updateTopicPartitionsCount: mockUpdateTopicPartitionsCount, - }); - const numberOfPartitionsEditForm = screen.getByRole('form', { - name: 'Edit number of partitions', - }); - - userEvent.type( - within(numberOfPartitionsEditForm).getByRole('spinbutton'), - '4' - ); - userEvent.click(within(numberOfPartitionsEditForm).getByRole('button')); - - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - await waitFor(() => clickOnDialogSubmitButton()); - - expect(mockUpdateTopicPartitionsCount).toHaveBeenCalledTimes(1); - }); - - it('calls updateTopicReplicationFactor', async () => { - const mockUpdateTopicReplicationFactor = jest.fn(); - renderComponent({ - updateTopicReplicationFactor: mockUpdateTopicReplicationFactor, - }); - - const replicationFactorEditForm = screen.getByRole('form', { - name: 'Edit replication factor', - }); - expect( - within(replicationFactorEditForm).getByRole('spinbutton', { - name: 'Replication Factor *', - }) - ).toBeInTheDocument(); - expect( - within(replicationFactorEditForm).getByRole('button', { name: 'Submit' }) - ).toBeInTheDocument(); - - userEvent.type( - within(replicationFactorEditForm).getByRole('spinbutton'), - '4' - ); - userEvent.click(within(replicationFactorEditForm).getByRole('button')); - - await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - await waitFor(() => clickOnDialogSubmitButton()); - - await waitFor(() => { - expect(mockUpdateTopicReplicationFactor).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx index bc13e8ec5f..4a9612504e 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/Edit/__test__/Edit.spec.tsx @@ -1,27 +1,39 @@ import React from 'react'; -import Edit, { Props } from 'components/Topics/Topic/Edit/Edit'; -import { screen } from '@testing-library/react'; +import Edit, { DEFAULTS, Props } from 'components/Topics/Topic/Edit/Edit'; +import { screen, waitFor } from '@testing-library/react'; import { render } from 'lib/testHelpers'; +import userEvent from '@testing-library/user-event'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { clusterTopicPath, clusterTopicsPath } from 'lib/paths'; import { topicName, clusterName, topicWithInfo } from './fixtures'; -const renderComponent = (props?: Partial) => +const historyMock = createMemoryHistory(); + +const renderComponent = (props: Partial = {}, history = historyMock) => render( - + + + ); -describe('DangerZone', () => { - it('renders', () => { +describe('Edit Component', () => { + it('renders the Edit Component', () => { renderComponent(); expect( @@ -31,4 +43,112 @@ describe('DangerZone', () => { screen.getByRole('heading', { name: `Danger Zone` }) ).toBeInTheDocument(); }); + + it('should check Edit component renders null is not rendered when topic is not passed', () => { + renderComponent({ topic: undefined }); + expect( + screen.queryByRole('heading', { name: `Edit ${topicName}` }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: `Danger Zone` }) + ).not.toBeInTheDocument(); + }); + + it('should check Edit component renders null is not isFetched is false', () => { + renderComponent({ isFetched: false }); + expect( + screen.queryByRole('heading', { name: `Edit ${topicName}` }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: `Danger Zone` }) + ).not.toBeInTheDocument(); + }); + + it('should check Edit component renders null is not topic config is not passed is false', () => { + const modifiedTopic = { ...topicWithInfo }; + modifiedTopic.config = undefined; + renderComponent({ topic: modifiedTopic }); + expect( + screen.queryByRole('heading', { name: `Edit ${topicName}` }) + ).not.toBeInTheDocument(); + expect( + screen.queryByRole('heading', { name: `Danger Zone` }) + ).not.toBeInTheDocument(); + }); + + describe('Edit Component with its topic default and modified values', () => { + it('should check the default partitions value in the DangerZone', () => { + renderComponent({ + topic: { ...topicWithInfo, partitionCount: undefined }, + }); + expect(screen.getByPlaceholderText('Number of partitions')).toHaveValue( + DEFAULTS.partitions + ); + }); + + it('should check the default partitions value in the DangerZone', () => { + renderComponent({ + topic: { ...topicWithInfo, replicationFactor: undefined }, + }); + expect(screen.getByPlaceholderText('Replication Factor')).toHaveValue( + DEFAULTS.replicationFactor + ); + }); + }); + + describe('Submit Case of the Edit Component', () => { + it('should check the submit functionality when topic updated is false', async () => { + const updateTopicMock = jest.fn(); + const mocked = createMemoryHistory({ + initialEntries: [`${clusterTopicsPath(clusterName)}/${topicName}/edit`], + }); + + jest.spyOn(mocked, 'push'); + renderComponent({ updateTopic: updateTopicMock }, mocked); + + const btn = screen.getAllByText(/submit/i)[0]; + expect(btn).toBeEnabled(); + + await waitFor(() => { + userEvent.type( + screen.getByPlaceholderText('Min In Sync Replicas'), + '1' + ); + userEvent.click(btn); + }); + expect(updateTopicMock).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(mocked.push).not.toHaveBeenCalled(); + }); + }); + + it('should check the submit functionality when topic updated is true', async () => { + const updateTopicMock = jest.fn(); + const mocked = createMemoryHistory({ + initialEntries: [`${clusterTopicsPath(clusterName)}/${topicName}/edit`], + }); + jest.spyOn(mocked, 'push'); + renderComponent( + { updateTopic: updateTopicMock, isTopicUpdated: true }, + mocked + ); + + const btn = screen.getAllByText(/submit/i)[0]; + + await waitFor(() => { + userEvent.type( + screen.getByPlaceholderText('Min In Sync Replicas'), + '1' + ); + userEvent.click(btn); + }); + expect(updateTopicMock).toHaveBeenCalledTimes(1); + await waitFor(() => { + expect(mocked.push).toHaveBeenCalled(); + expect(mocked.location.pathname).toBe( + clusterTopicPath(clusterName, topicName) + ); + }); + }); + }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx index c31d031d38..8cd8409bae 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/SendMessage.spec.tsx @@ -18,9 +18,14 @@ import { store } from 'redux/store'; import { fetchTopicDetailsAction } from 'redux/actions'; import { initialState } from 'redux/reducers/topics/reducer'; import { externalTopicPayload } from 'redux/reducers/topics/__test__/fixtures'; +import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage'; +import Alerts from 'components/Alerts/Alerts'; +import * as S from 'components/App.styled'; import { testSchema } from './fixtures'; +import Mock = jest.Mock; + jest.mock('json-schema-faker', () => ({ generate: () => ({ f1: -93251214, @@ -30,6 +35,10 @@ jest.mock('json-schema-faker', () => ({ option: jest.fn(), })); +jest.mock('components/Topics/Topic/SendMessage/validateMessage', () => + jest.fn() +); + const clusterName = 'testCluster'; const topicName = externalTopicPayload.name; const history = createMemoryHistory(); @@ -37,15 +46,30 @@ const history = createMemoryHistory(); const renderComponent = () => { history.push(clusterTopicSendMessagePath(clusterName, topicName)); render( - - - - - , + <> + + + + + + + + + , { store } ); }; +const renderAndSubmitData = async (error: string[] = []) => { + renderComponent(); + await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); + + userEvent.selectOptions(screen.getByLabelText('Partition'), '0'); + const sendBtn = await screen.findByText('Send'); + (validateMessage as Mock).mockImplementation(() => error); + userEvent.click(sendBtn); +}; + describe('SendMessage', () => { beforeAll(() => { store.dispatch( @@ -71,6 +95,8 @@ describe('SendMessage', () => { }); describe('when schema is fetched', () => { + const url = `/api/clusters/${clusterName}/topics/${topicName}/messages`; + beforeEach(() => { fetchMock.getOnce( `/api/clusters/${clusterName}/topics/${topicName}/messages/schema`, @@ -79,20 +105,41 @@ describe('SendMessage', () => { }); it('calls sendTopicMessage on submit', async () => { - const sendTopicMessageMock = fetchMock.postOnce( - `/api/clusters/${clusterName}/topics/${topicName}/messages`, - 200 - ); - renderComponent(); - await waitForElementToBeRemoved(() => screen.getByRole('progressbar')); + const sendTopicMessageMock = fetchMock.postOnce(url, 200); + await renderAndSubmitData(); - userEvent.selectOptions(screen.getByLabelText('Partition'), '0'); - await screen.findByText('Send'); - userEvent.click(screen.getByText('Send')); - await waitFor(() => expect(sendTopicMessageMock.called()).toBeTruthy()); + await waitFor(() => + expect(sendTopicMessageMock.called(url)).toBeTruthy() + ); expect(history.location.pathname).toEqual( clusterTopicMessagesPath(clusterName, topicName) ); }); + + it('should make the sendTopicMessage but most find an error within it', async () => { + const sendTopicMessageMock = fetchMock.postOnce(url, { + throws: 'Error', + }); + await renderAndSubmitData(); + await waitFor(() => { + expect(sendTopicMessageMock.called(url)).toBeTruthy(); + }); + await waitFor(() => { + expect(screen.getByRole('alert')).toBeInTheDocument(); + }); + expect(history.location.pathname).toEqual( + clusterTopicMessagesPath(clusterName, topicName) + ); + }); + + it('should check and view validation error message when is not valid', async () => { + const sendTopicMessageMock = fetchMock.postOnce(url, 200); + await renderAndSubmitData(['error']); + + await waitFor(() => expect(sendTopicMessageMock.called(url)).toBeFalsy()); + expect(history.location.pathname).not.toEqual( + clusterTopicMessagesPath(clusterName, topicName) + ); + }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/validateMessage.spec.ts b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/validateMessage.spec.ts index 4014625ef0..520fd035f3 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/validateMessage.spec.ts +++ b/kafka-ui-react-app/src/components/Topics/Topic/SendMessage/__test__/validateMessage.spec.ts @@ -1,12 +1,62 @@ import validateMessage from 'components/Topics/Topic/SendMessage/validateMessage'; +import cloneDeep from 'lodash/cloneDeep'; import { testSchema } from './fixtures'; describe('validateMessage', () => { - it('returns no errors on correct input data', () => { + const defaultValidKey = `{"f1": 32, "f2": "multi-state", "schema": "Bedfordshire violet SAS"}`; + const defaultValidContent = `{"f1": 21128, "f2": "Health Berkshire", "schema": "Dynamic"}`; + + it('should return empty error data if value is empty', () => { + const key = ``; + const content = ``; + expect(validateMessage(key, content, testSchema)).toEqual([]); + }); + + it('should return empty error data if schema is empty', () => { const key = `{"f1": 32, "f2": "multi-state", "schema": "Bedfordshire violet SAS"}`; const content = `{"f1": 21128, "f2": "Health Berkshire", "schema": "Dynamic"}`; - expect(validateMessage(key, content, testSchema)).toEqual([]); + const schema = cloneDeep(testSchema); + schema.key.schema = ''; + schema.value.schema = ''; + expect(validateMessage(key, content, schema)).toEqual([]); + }); + + it('should return parsing error data if schema is not parsed with type of key', () => { + const schema = cloneDeep(testSchema); + schema.key.schema = '{invalid'; + expect( + validateMessage(defaultValidKey, defaultValidContent, schema) + ).toEqual([`Error in parsing the "key" field schema`]); + }); + + it('should return parsing error data if schema is not parsed with type of value', () => { + const schema = cloneDeep(testSchema); + schema.value.schema = '{invalid'; + expect( + validateMessage(defaultValidKey, defaultValidContent, schema) + ).toEqual([`Error in parsing the "content" field schema`]); + }); + + it('should return empty error data if schema type is string', () => { + const schema = cloneDeep(testSchema); + schema.key.schema = `{"type": "string"}`; + schema.value.schema = `{"type": "string"}`; + expect( + validateMessage(defaultValidKey, defaultValidContent, schema) + ).toEqual([]); + }); + + it('should return error data if compile Ajv data throws an error', () => { + expect( + validateMessage(defaultValidKey, defaultValidContent, testSchema) + ).toEqual([]); + }); + + it('returns no errors on correct input data', () => { + expect( + validateMessage(defaultValidContent, defaultValidContent, testSchema) + ).toEqual([]); }); it('returns errors on invalid input data', () => { diff --git a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx b/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx index eaea042156..97ff07b9f3 100644 --- a/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/Topic/__tests__/Topic.spec.tsx @@ -9,64 +9,78 @@ import { clusterTopicSendMessagePath, } from 'lib/paths'; +const topicText = { + edit: 'Edit Container', + send: 'Send Message', + detail: 'Details Container', + loading: 'Loading', +}; + jest.mock('components/Topics/Topic/Edit/EditContainer', () => () => ( -
Edit Container
+
{topicText.edit}
)); jest.mock('components/Topics/Topic/SendMessage/SendMessage', () => () => ( -
Send Message
+
{topicText.send}
)); jest.mock('components/Topics/Topic/Details/DetailsContainer', () => () => ( -
Details Container
+
{topicText.detail}
)); jest.mock('components/common/PageLoader/PageLoader', () => () => ( -
Loading
+
{topicText.loading}
)); -const resetTopicMessages = jest.fn(); -const fetchTopicDetailsMock = jest.fn(); +describe('Topic Component', () => { + const resetTopicMessages = jest.fn(); + const fetchTopicDetailsMock = jest.fn(); -const renderComponent = (pathname: string, topicFetching: boolean) => - render( - - - , - { pathname } - ); + const renderComponent = (pathname: string, topicFetching: boolean) => + render( + + + , + { pathname } + ); -it('renders Edit page', () => { - renderComponent(clusterTopicEditPath('local', 'myTopicName'), false); - expect(screen.getByText('Edit Container')).toBeInTheDocument(); -}); - -it('renders Send Message page', () => { - renderComponent(clusterTopicSendMessagePath('local', 'myTopicName'), false); - expect(screen.getByText('Send Message')).toBeInTheDocument(); -}); - -it('renders Details Container page', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), false); - expect(screen.getByText('Details Container')).toBeInTheDocument(); -}); - -it('renders Page loader', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), true); - expect(screen.getByText('Loading')).toBeInTheDocument(); -}); - -it('fetches topicDetails', () => { - renderComponent(clusterTopicPath('local', 'myTopicName'), false); - expect(fetchTopicDetailsMock).toHaveBeenCalledTimes(1); -}); - -it('resets topic messages after unmount', () => { - const component = renderComponent( - clusterTopicPath('local', 'myTopicName'), - false - ); - component.unmount(); - expect(resetTopicMessages).toHaveBeenCalledTimes(1); + afterEach(() => { + resetTopicMessages.mockClear(); + fetchTopicDetailsMock.mockClear(); + }); + + it('renders Edit page', () => { + renderComponent(clusterTopicEditPath('local', 'myTopicName'), false); + expect(screen.getByText(topicText.edit)).toBeInTheDocument(); + }); + + it('renders Send Message page', () => { + renderComponent(clusterTopicSendMessagePath('local', 'myTopicName'), false); + expect(screen.getByText(topicText.send)).toBeInTheDocument(); + }); + + it('renders Details Container page', () => { + renderComponent(clusterTopicPath('local', 'myTopicName'), false); + expect(screen.getByText(topicText.detail)).toBeInTheDocument(); + }); + + it('renders Page loader', () => { + renderComponent(clusterTopicPath('local', 'myTopicName'), true); + expect(screen.getByText(topicText.loading)).toBeInTheDocument(); + }); + + it('fetches topicDetails', () => { + renderComponent(clusterTopicPath('local', 'myTopicName'), false); + expect(fetchTopicDetailsMock).toHaveBeenCalledTimes(1); + }); + + it('resets topic messages after unmount', () => { + const component = renderComponent( + clusterTopicPath('local', 'myTopicName'), + false + ); + component.unmount(); + expect(resetTopicMessages).toHaveBeenCalledTimes(1); + }); }); diff --git a/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx new file mode 100644 index 0000000000..e76230bef5 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/__tests__/Topics.spec.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import Topics from 'components/Topics/Topics'; +import { Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { screen } from '@testing-library/react'; +import { + clusterTopicCopyPath, + clusterTopicNewPath, + clusterTopicPath, + clusterTopicsPath, +} from 'lib/paths'; + +const listContainer = 'listContainer'; +const topicContainer = 'topicContainer'; +const newCopyContainer = 'newCopyContainer'; + +jest.mock('components/Topics/List/ListContainer', () => () => ( +
{listContainer}
+)); +jest.mock('components/Topics/Topic/TopicContainer', () => () => ( +
{topicContainer}
+)); +jest.mock('components/Topics/New/New', () => () => ( +
{newCopyContainer}
+)); + +describe('Topics Component', () => { + const clusterName = 'clusterName'; + const topicName = 'topicName'; + const setUpComponent = (path: string) => { + const history = createMemoryHistory({ + initialEntries: [path], + }); + return render( + + + + ); + }; + + it('should check if the page is Topics List rendered', () => { + setUpComponent(clusterTopicsPath(clusterName)); + expect(screen.getByText(listContainer)).toBeInTheDocument(); + }); + + it('should check if the page is New Topic rendered', () => { + setUpComponent(clusterTopicNewPath(clusterName)); + expect(screen.getByText(newCopyContainer)).toBeInTheDocument(); + }); + + it('should check if the page is Copy Topic rendered', () => { + setUpComponent(clusterTopicCopyPath(clusterName)); + expect(screen.getByText(newCopyContainer)).toBeInTheDocument(); + }); + + it('should check if the page is Topic page rendered', () => { + setUpComponent(clusterTopicPath(clusterName, topicName)); + expect(screen.getByText(topicContainer)).toBeInTheDocument(); + }); +}); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx index 0cc39496e8..dde7e59b13 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParamField.spec.tsx @@ -12,8 +12,6 @@ const isDisabled = false; const index = 0; const existingFields: string[] = []; const field = { name: 'name', value: 'value', id: 'id' }; -const remove = jest.fn(); -const setExistingFields = jest.fn(); const SPACE_KEY = ' '; @@ -23,6 +21,9 @@ const selectOption = async (listbox: HTMLElement, option: string) => { }; describe('CustomParamsField', () => { + const remove = jest.fn(); + const setExistingFields = jest.fn(); + const setupComponent = (props: Props) => { const Wrapper: React.FC = ({ children }) => { const methods = useForm(); @@ -36,7 +37,12 @@ describe('CustomParamsField', () => { ); }; - it('renders with props', () => { + afterEach(() => { + remove.mockClear(); + setExistingFields.mockClear(); + }); + + it('renders the component with its view correctly', () => { setupComponent({ field, isDisabled, @@ -61,7 +67,7 @@ describe('CustomParamsField', () => { setExistingFields, }); userEvent.click(screen.getByRole('button')); - expect(remove.mock.calls.length).toBe(1); + expect(remove).toHaveBeenCalledTimes(1); }); it('pressing space on button triggers remove', () => { @@ -75,7 +81,7 @@ describe('CustomParamsField', () => { }); userEvent.type(screen.getByRole('button'), SPACE_KEY); // userEvent.type triggers remove two times as at first it clicks on element and then presses space - expect(remove.mock.calls.length).toBe(2); + expect(remove).toHaveBeenCalledTimes(2); }); it('can select option', async () => { @@ -123,7 +129,7 @@ describe('CustomParamsField', () => { const listbox = screen.getByRole('listbox'); await selectOption(listbox, 'compression.type'); - expect(setExistingFields.mock.calls.length).toBe(1); + expect(setExistingFields).toHaveBeenCalledTimes(1); }); }); }); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx index 6360862575..e6965779ee 100644 --- a/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/CustomParams/__test__/CustomParams.spec.tsx @@ -80,14 +80,14 @@ describe('CustomParams', () => { }); describe('works with user inputs correctly', () => { - beforeEach(() => { + let button: HTMLButtonElement; + beforeEach(async () => { renderComponent({ isSubmitting: false }); + button = screen.getByRole('button'); + await waitFor(() => userEvent.click(button)); }); it('button click creates custom param fieldset', async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); - const listbox = screen.getByRole('listbox'); expect(listbox).toBeInTheDocument(); @@ -96,8 +96,6 @@ describe('CustomParams', () => { }); it('can select option', async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); const listbox = screen.getByRole('listbox'); await selectOption(listbox, 'compression.type'); @@ -109,9 +107,6 @@ describe('CustomParams', () => { }); it('when selected option changes disabled options update correctly', async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); - const listbox = screen.getByRole('listbox'); await selectOption(listbox, 'compression.type'); @@ -124,8 +119,6 @@ describe('CustomParams', () => { }); it('multiple button clicks create multiple fieldsets', async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); await waitFor(() => userEvent.click(button)); await waitFor(() => userEvent.click(button)); @@ -137,8 +130,6 @@ describe('CustomParams', () => { }); it("can't select already selected option", async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); await waitFor(() => userEvent.click(button)); const listboxes = screen.getAllByRole('listbox'); @@ -152,8 +143,6 @@ describe('CustomParams', () => { }); it('when fieldset with selected custom property type is deleted disabled options update correctly', async () => { - const button = screen.getByRole('button'); - await waitFor(() => userEvent.click(button)); await waitFor(() => userEvent.click(button)); await waitFor(() => userEvent.click(button)); diff --git a/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx new file mode 100644 index 0000000000..c0ff8e50d0 --- /dev/null +++ b/kafka-ui-react-app/src/components/Topics/shared/Form/__tests__/TopicForm.styled.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render } from 'lib/testHelpers'; +import * as S from 'components/Topics/shared/Form/TopicForm.styled'; +import { screen } from '@testing-library/react'; +import theme from 'theme/theme'; + +describe('TopicForm styled components', () => { + describe('Button', () => { + it('should check the button styling in isActive state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveStyle({ + border: `1px solid ${theme.button.border.active}`, + backgroundColor: theme.button.primary.backgroundColor.active, + }); + }); + + it('should check the button styling in non Active state', () => { + render(); + const button = screen.getByRole('button'); + expect(button).toHaveStyle({ + border: `1px solid ${theme.button.primary.color}`, + backgroundColor: theme.button.primary.backgroundColor.normal, + }); + }); + }); +}); diff --git a/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts b/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts index d6d6f601a8..cec6cf350d 100644 --- a/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts +++ b/kafka-ui-react-app/src/redux/reducers/topics/__test__/fixtures.ts @@ -1,3 +1,5 @@ +import { Topic } from 'generated-sources'; + export const internalTopicPayload = { name: '__internal.topic', internal: true, @@ -40,4 +42,7 @@ export const externalTopicPayload = { ], }; -export const topicsPayload = [internalTopicPayload, externalTopicPayload]; +export const topicsPayload: Topic[] = [ + internalTopicPayload, + externalTopicPayload, +];