Issues/1740 live tailing improvements (#1774)
* Implementing Context to the Topic messages pages * Using TopicContext in the Topics Topic MessageTable component * Using TopicContext variable in the Filters component * Fixing the Ordering of the Live mode Topic messaging * Fixing isLive parameter bug during page refresh * Minor code modification in Topic Filter Message page * Implement the correct seekType during live mode in url as well as in api call * Add Test cases to Messages and refactor eventSource Mock * Add initial Testing file for messages table * improve the MessagesTable test File * improve the MessagesTable test File + Filter Test File * improve the MessagesTable test File * Change the function name toggleSeekDirection to changeSeekDirection * change the name of the test suites to be more declarative * Display the table progress bar in live mode only when no data is fetched
This commit is contained in:
parent
c79905ce32
commit
68f8eed8f8
10 changed files with 325 additions and 96 deletions
|
@ -1,4 +1,4 @@
|
||||||
import { render } from 'lib/testHelpers';
|
import { render, EventSourceMock } from 'lib/testHelpers';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Query, {
|
import Query, {
|
||||||
getFormattedErrorFromTableData,
|
getFormattedErrorFromTableData,
|
||||||
|
@ -20,27 +20,6 @@ const renderComponent = () =>
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Small mock to get rid of reference error
|
|
||||||
class EventSourceMock {
|
|
||||||
url: string;
|
|
||||||
|
|
||||||
close: () => void;
|
|
||||||
|
|
||||||
open: () => void;
|
|
||||||
|
|
||||||
error: () => void;
|
|
||||||
|
|
||||||
onmessage: () => void;
|
|
||||||
|
|
||||||
constructor(url: string) {
|
|
||||||
this.url = url;
|
|
||||||
this.open = jest.fn();
|
|
||||||
this.error = jest.fn();
|
|
||||||
this.onmessage = jest.fn();
|
|
||||||
this.close = jest.fn();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('Query', () => {
|
describe('Query', () => {
|
||||||
it('renders', () => {
|
it('renders', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
TopicMessageEventTypeEnum,
|
TopicMessageEventTypeEnum,
|
||||||
MessageFilterType,
|
MessageFilterType,
|
||||||
} from 'generated-sources';
|
} from 'generated-sources';
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { omitBy } from 'lodash';
|
import { omitBy } from 'lodash';
|
||||||
import { useHistory, useLocation } from 'react-router';
|
import { useHistory, useLocation } from 'react-router';
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
|
@ -25,6 +25,8 @@ import { Button } from 'components/common/Button/Button';
|
||||||
import FilterModal, {
|
import FilterModal, {
|
||||||
FilterEdit,
|
FilterEdit,
|
||||||
} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
|
} from 'components/Topics/Topic/Details/Messages/Filters/FilterModal';
|
||||||
|
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
|
||||||
|
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
|
||||||
|
|
||||||
import * as S from './Filters.styled';
|
import * as S from './Filters.styled';
|
||||||
import {
|
import {
|
||||||
|
@ -66,11 +68,6 @@ export const SeekTypeOptions = [
|
||||||
{ value: SeekType.OFFSET, label: 'Offset' },
|
{ value: SeekType.OFFSET, label: 'Offset' },
|
||||||
{ value: SeekType.TIMESTAMP, label: 'Timestamp' },
|
{ value: SeekType.TIMESTAMP, label: 'Timestamp' },
|
||||||
];
|
];
|
||||||
export const SeekDirectionOptions = [
|
|
||||||
{ value: SeekDirection.FORWARD, label: 'Oldest First', isLive: false },
|
|
||||||
{ value: SeekDirection.BACKWARD, label: 'Newest First', isLive: false },
|
|
||||||
{ value: SeekDirection.TAILING, label: 'Live Mode', isLive: true },
|
|
||||||
];
|
|
||||||
|
|
||||||
const Filters: React.FC<FiltersProps> = ({
|
const Filters: React.FC<FiltersProps> = ({
|
||||||
clusterName,
|
clusterName,
|
||||||
|
@ -88,16 +85,14 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const { searchParams, seekDirection, isLive, changeSeekDirection } =
|
||||||
|
useContext(TopicMessagesContext);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
const toggleIsOpen = () => setIsOpen(!isOpen);
|
const toggleIsOpen = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
const source = React.useRef<EventSource | null>(null);
|
const source = React.useRef<EventSource | null>(null);
|
||||||
|
|
||||||
const searchParams = React.useMemo(
|
|
||||||
() => new URLSearchParams(location.search),
|
|
||||||
[location]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
|
const [selectedPartitions, setSelectedPartitions] = React.useState<Option[]>(
|
||||||
getSelectedPartitionsFromSeekToParam(searchParams, partitions)
|
getSelectedPartitionsFromSeekToParam(searchParams, partitions)
|
||||||
);
|
);
|
||||||
|
@ -132,10 +127,6 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
: MessageFilterType.STRING_CONTAINS
|
: MessageFilterType.STRING_CONTAINS
|
||||||
);
|
);
|
||||||
const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');
|
const [query, setQuery] = React.useState<string>(searchParams.get('q') || '');
|
||||||
const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(
|
|
||||||
(searchParams.get('seekDirection') as SeekDirection) ||
|
|
||||||
SeekDirection.FORWARD
|
|
||||||
);
|
|
||||||
const isSeekTypeControlVisible = React.useMemo(
|
const isSeekTypeControlVisible = React.useMemo(
|
||||||
() => selectedPartitions.length > 0,
|
() => selectedPartitions.length > 0,
|
||||||
[selectedPartitions]
|
[selectedPartitions]
|
||||||
|
@ -178,7 +169,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
setAttempt(attempt + 1);
|
setAttempt(attempt + 1);
|
||||||
|
|
||||||
if (isSeekTypeControlVisible) {
|
if (isSeekTypeControlVisible) {
|
||||||
props.seekType = currentSeekType;
|
props.seekType = isLive ? SeekType.LATEST : currentSeekType;
|
||||||
props.seekTo = selectedPartitions.map(({ value }) => {
|
props.seekTo = selectedPartitions.map(({ value }) => {
|
||||||
let seekToOffset;
|
let seekToOffset;
|
||||||
|
|
||||||
|
@ -217,21 +208,6 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
query,
|
query,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toggleSeekDirection = (val: string) => {
|
|
||||||
switch (val) {
|
|
||||||
case SeekDirection.FORWARD:
|
|
||||||
setSeekDirection(SeekDirection.FORWARD);
|
|
||||||
break;
|
|
||||||
case SeekDirection.BACKWARD:
|
|
||||||
setSeekDirection(SeekDirection.BACKWARD);
|
|
||||||
break;
|
|
||||||
case SeekDirection.TAILING:
|
|
||||||
setSeekDirection(SeekDirection.TAILING);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSSECancel = () => {
|
const handleSSECancel = () => {
|
||||||
if (!source.current) return;
|
if (!source.current) return;
|
||||||
|
|
||||||
|
@ -295,7 +271,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (location.search.length !== 0) {
|
if (location.search?.length !== 0) {
|
||||||
const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/topics/${topicName}/messages${location.search}`;
|
const url = `${BASE_PARAMS.basePath}/api/clusters/${clusterName}/topics/${topicName}/messages${location.search}`;
|
||||||
const sse = new EventSource(url);
|
const sse = new EventSource(url);
|
||||||
|
|
||||||
|
@ -346,7 +322,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
updatePhase,
|
updatePhase,
|
||||||
]);
|
]);
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (location.search.length === 0) {
|
if (location.search?.length === 0) {
|
||||||
handleFiltersSubmit();
|
handleFiltersSubmit();
|
||||||
}
|
}
|
||||||
}, [handleFiltersSubmit, location]);
|
}, [handleFiltersSubmit, location]);
|
||||||
|
@ -376,7 +352,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
selectSize="M"
|
selectSize="M"
|
||||||
minWidth="100px"
|
minWidth="100px"
|
||||||
options={SeekTypeOptions}
|
options={SeekTypeOptions}
|
||||||
disabled={seekDirection === SeekDirection.TAILING}
|
disabled={isLive}
|
||||||
/>
|
/>
|
||||||
{currentSeekType === SeekType.OFFSET ? (
|
{currentSeekType === SeekType.OFFSET ? (
|
||||||
<Input
|
<Input
|
||||||
|
@ -387,7 +363,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
className="offset-selector"
|
className="offset-selector"
|
||||||
placeholder="Offset"
|
placeholder="Offset"
|
||||||
onChange={({ target: { value } }) => setOffset(value)}
|
onChange={({ target: { value } }) => setOffset(value)}
|
||||||
disabled={seekDirection === SeekDirection.TAILING}
|
disabled={isLive}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DatePicker
|
<DatePicker
|
||||||
|
@ -398,7 +374,7 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
dateFormat="MMMM d, yyyy HH:mm"
|
dateFormat="MMMM d, yyyy HH:mm"
|
||||||
className="date-picker"
|
className="date-picker"
|
||||||
placeholderText="Select timestamp"
|
placeholderText="Select timestamp"
|
||||||
disabled={seekDirection === SeekDirection.TAILING}
|
disabled={isLive}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</S.SeekTypeSelectorWrapper>
|
</S.SeekTypeSelectorWrapper>
|
||||||
|
@ -440,11 +416,11 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
</S.FilterInputs>
|
</S.FilterInputs>
|
||||||
<Select
|
<Select
|
||||||
selectSize="M"
|
selectSize="M"
|
||||||
onChange={(option) => toggleSeekDirection(option as string)}
|
onChange={(option) => changeSeekDirection(option as string)}
|
||||||
value={seekDirection}
|
value={seekDirection}
|
||||||
minWidth="120px"
|
minWidth="120px"
|
||||||
options={SeekDirectionOptions}
|
options={SeekDirectionOptions}
|
||||||
isLive={seekDirection === SeekDirection.TAILING}
|
isLive={isLive}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<S.ActiveSmartFilterWrapper>
|
<S.ActiveSmartFilterWrapper>
|
||||||
|
@ -479,12 +455,12 @@ const Filters: React.FC<FiltersProps> = ({
|
||||||
isFetching &&
|
isFetching &&
|
||||||
phaseMessage}
|
phaseMessage}
|
||||||
</p>
|
</p>
|
||||||
<S.MessageLoading isLive={seekDirection === SeekDirection.TAILING}>
|
<S.MessageLoading isLive={isLive}>
|
||||||
<S.MessageLoadingSpinner isFetching={isFetching} />
|
<S.MessageLoadingSpinner isFetching={isFetching} />
|
||||||
Loading messages.
|
Loading messages.
|
||||||
<S.StopLoading
|
<S.StopLoading
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSeekDirection(SeekDirection.FORWARD);
|
changeSeekDirection(SeekDirection.FORWARD);
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { SeekDirectionOptions } from 'components/Topics/Topic/Details/Messages/Messages';
|
||||||
import Filters, {
|
import Filters, {
|
||||||
FiltersProps,
|
FiltersProps,
|
||||||
SeekDirectionOptions,
|
|
||||||
SeekTypeOptions,
|
SeekTypeOptions,
|
||||||
} from 'components/Topics/Topic/Details/Messages/Filters/Filters';
|
} from 'components/Topics/Topic/Details/Messages/Filters/Filters';
|
||||||
import { render } from 'lib/testHelpers';
|
import { render } from 'lib/testHelpers';
|
||||||
import { screen, waitFor, within } from '@testing-library/react';
|
import { screen, waitFor, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import TopicMessagesContext, {
|
||||||
|
ContextProps,
|
||||||
|
} from 'components/contexts/TopicMessagesContext';
|
||||||
|
import { SeekDirection } from 'generated-sources';
|
||||||
|
|
||||||
const setupWrapper = (props?: Partial<FiltersProps>) =>
|
const defaultContextValue: ContextProps = {
|
||||||
|
isLive: false,
|
||||||
|
seekDirection: SeekDirection.FORWARD,
|
||||||
|
searchParams: new URLSearchParams(''),
|
||||||
|
changeSeekDirection: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupWrapper = (
|
||||||
|
props: Partial<FiltersProps> = {},
|
||||||
|
ctx: ContextProps = defaultContextValue
|
||||||
|
) => {
|
||||||
render(
|
render(
|
||||||
|
<TopicMessagesContext.Provider value={ctx}>
|
||||||
<Filters
|
<Filters
|
||||||
clusterName="test-cluster"
|
clusterName="test-cluster"
|
||||||
topicName="test-topic"
|
topicName="test-topic"
|
||||||
|
@ -23,7 +38,9 @@ const setupWrapper = (props?: Partial<FiltersProps>) =>
|
||||||
setIsFetching={jest.fn()}
|
setIsFetching={jest.fn()}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</TopicMessagesContext.Provider>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
describe('Filters component', () => {
|
describe('Filters component', () => {
|
||||||
it('renders component', () => {
|
it('renders component', () => {
|
||||||
setupWrapper();
|
setupWrapper();
|
||||||
|
|
|
@ -1,13 +1,84 @@
|
||||||
import React from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
|
||||||
|
import { SeekDirection } from 'generated-sources';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
import FiltersContainer from './Filters/FiltersContainer';
|
import FiltersContainer from './Filters/FiltersContainer';
|
||||||
import MessagesTable from './MessagesTable';
|
import MessagesTable from './MessagesTable';
|
||||||
|
|
||||||
const Messages: React.FC = () => (
|
export const SeekDirectionOptionsObj = {
|
||||||
<div>
|
[SeekDirection.FORWARD]: {
|
||||||
<FiltersContainer />
|
value: SeekDirection.FORWARD,
|
||||||
<MessagesTable />
|
label: 'Oldest First',
|
||||||
</div>
|
isLive: false,
|
||||||
|
},
|
||||||
|
[SeekDirection.BACKWARD]: {
|
||||||
|
value: SeekDirection.BACKWARD,
|
||||||
|
label: 'Newest First',
|
||||||
|
isLive: false,
|
||||||
|
},
|
||||||
|
[SeekDirection.TAILING]: {
|
||||||
|
value: SeekDirection.TAILING,
|
||||||
|
label: 'Live Mode',
|
||||||
|
isLive: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SeekDirectionOptions = Object.values(SeekDirectionOptionsObj);
|
||||||
|
|
||||||
|
const Messages: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const searchParams = React.useMemo(
|
||||||
|
() => new URLSearchParams(location.search),
|
||||||
|
[location.search]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const defaultSeekValue = SeekDirectionOptions[0];
|
||||||
|
|
||||||
|
const [seekDirection, setSeekDirection] = React.useState<SeekDirection>(
|
||||||
|
(searchParams.get('seekDirection') as SeekDirection) ||
|
||||||
|
defaultSeekValue.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isLive, setIsLive] = useState<boolean>(
|
||||||
|
SeekDirectionOptionsObj[seekDirection].isLive
|
||||||
|
);
|
||||||
|
|
||||||
|
const changeSeekDirection = useCallback((val: string) => {
|
||||||
|
switch (val) {
|
||||||
|
case SeekDirection.FORWARD:
|
||||||
|
setSeekDirection(SeekDirection.FORWARD);
|
||||||
|
setIsLive(SeekDirectionOptionsObj[SeekDirection.FORWARD].isLive);
|
||||||
|
break;
|
||||||
|
case SeekDirection.BACKWARD:
|
||||||
|
setSeekDirection(SeekDirection.BACKWARD);
|
||||||
|
setIsLive(SeekDirectionOptionsObj[SeekDirection.BACKWARD].isLive);
|
||||||
|
break;
|
||||||
|
case SeekDirection.TAILING:
|
||||||
|
setSeekDirection(SeekDirection.TAILING);
|
||||||
|
setIsLive(SeekDirectionOptionsObj[SeekDirection.TAILING].isLive);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
seekDirection,
|
||||||
|
searchParams,
|
||||||
|
changeSeekDirection,
|
||||||
|
isLive,
|
||||||
|
}),
|
||||||
|
[seekDirection, searchParams, changeSeekDirection]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopicMessagesContext.Provider value={contextValue}>
|
||||||
|
<FiltersContainer />
|
||||||
|
<MessagesTable />
|
||||||
|
</TopicMessagesContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Messages;
|
export default Messages;
|
||||||
|
|
|
@ -4,13 +4,14 @@ import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeader
|
||||||
import { SeekDirection, TopicMessage } from 'generated-sources';
|
import { SeekDirection, TopicMessage } from 'generated-sources';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { compact, concat, groupBy, map, maxBy, minBy } from 'lodash';
|
import { compact, concat, groupBy, map, maxBy, minBy } from 'lodash';
|
||||||
import React from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { useHistory, useLocation } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import {
|
import {
|
||||||
getTopicMessges,
|
getTopicMessges,
|
||||||
getIsTopicMessagesFetching,
|
getIsTopicMessagesFetching,
|
||||||
} from 'redux/reducers/topicMessages/selectors';
|
} from 'redux/reducers/topicMessages/selectors';
|
||||||
|
import TopicMessagesContext from 'components/contexts/TopicMessagesContext';
|
||||||
|
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
import * as S from './MessageContent/MessageContent.styled';
|
import * as S from './MessageContent/MessageContent.styled';
|
||||||
|
@ -22,13 +23,9 @@ const MessagesPaginationWrapperStyled = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MessagesTable: React.FC = () => {
|
const MessagesTable: React.FC = () => {
|
||||||
const location = useLocation();
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const searchParams = React.useMemo(
|
const { searchParams, isLive } = useContext(TopicMessagesContext);
|
||||||
() => new URLSearchParams(location.search),
|
|
||||||
[location]
|
|
||||||
);
|
|
||||||
|
|
||||||
const messages = useSelector(getTopicMessges);
|
const messages = useSelector(getTopicMessges);
|
||||||
const isFetching = useSelector(getIsTopicMessagesFetching);
|
const isFetching = useSelector(getIsTopicMessagesFetching);
|
||||||
|
@ -94,7 +91,7 @@ const MessagesTable: React.FC = () => {
|
||||||
message={message}
|
message={message}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isFetching && (
|
{(isFetching || isLive) && !messages.length && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={10}>
|
<td colSpan={10}>
|
||||||
<PageLoader />
|
<PageLoader />
|
||||||
|
@ -108,9 +105,13 @@ const MessagesTable: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isLive && (
|
||||||
<MessagesPaginationWrapperStyled>
|
<MessagesPaginationWrapperStyled>
|
||||||
<S.PaginationButton onClick={handleNextClick}>Next</S.PaginationButton>
|
<S.PaginationButton onClick={handleNextClick}>
|
||||||
|
Next
|
||||||
|
</S.PaginationButton>
|
||||||
</MessagesPaginationWrapperStyled>
|
</MessagesPaginationWrapperStyled>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import { render, EventSourceMock } from 'lib/testHelpers';
|
||||||
|
import Messages, {
|
||||||
|
SeekDirectionOptions,
|
||||||
|
SeekDirectionOptionsObj,
|
||||||
|
} from 'components/Topics/Topic/Details/Messages/Messages';
|
||||||
|
import { Router } from 'react-router-dom';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { SeekDirection, SeekType } from 'generated-sources';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
describe('Messages', () => {
|
||||||
|
const searchParams = `?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`;
|
||||||
|
|
||||||
|
const setUpComponent = (param: string = searchParams) => {
|
||||||
|
const history = createMemoryHistory();
|
||||||
|
history.push({
|
||||||
|
search: new URLSearchParams(param).toString(),
|
||||||
|
});
|
||||||
|
return render(
|
||||||
|
<Router history={history}>
|
||||||
|
<Messages />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Object.defineProperty(window, 'EventSource', {
|
||||||
|
value: EventSourceMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('component rendering default behavior with the search params', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpComponent();
|
||||||
|
});
|
||||||
|
it('should check default seekDirection if it actually take the value from the url', () => {
|
||||||
|
expect(screen.getByRole('listbox')).toHaveTextContent(
|
||||||
|
SeekDirectionOptionsObj[SeekDirection.FORWARD].label
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the SeekDirection select changes', () => {
|
||||||
|
const seekDirectionSelect = screen.getByRole('listbox');
|
||||||
|
const seekDirectionOption = screen.getByRole('option');
|
||||||
|
|
||||||
|
expect(seekDirectionOption).toHaveTextContent(
|
||||||
|
SeekDirectionOptionsObj[SeekDirection.FORWARD].label
|
||||||
|
);
|
||||||
|
|
||||||
|
const labelValue1 = SeekDirectionOptions[1].label;
|
||||||
|
userEvent.click(seekDirectionSelect);
|
||||||
|
userEvent.selectOptions(seekDirectionSelect, [
|
||||||
|
SeekDirectionOptions[1].label,
|
||||||
|
]);
|
||||||
|
expect(seekDirectionOption).toHaveTextContent(labelValue1);
|
||||||
|
|
||||||
|
const labelValue0 = SeekDirectionOptions[0].label;
|
||||||
|
userEvent.click(seekDirectionSelect);
|
||||||
|
userEvent.selectOptions(seekDirectionSelect, [
|
||||||
|
SeekDirectionOptions[0].label,
|
||||||
|
]);
|
||||||
|
expect(seekDirectionOption).toHaveTextContent(labelValue0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Component rendering with custom Url search params', () => {
|
||||||
|
it('reacts to a change of seekDirection in the url which make the select pick up different value', () => {
|
||||||
|
setUpComponent(
|
||||||
|
searchParams.replace(SeekDirection.FORWARD, SeekDirection.BACKWARD)
|
||||||
|
);
|
||||||
|
expect(screen.getByRole('listbox')).toHaveTextContent(
|
||||||
|
SeekDirectionOptionsObj[SeekDirection.BACKWARD].label
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
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 userEvent from '@testing-library/user-event';
|
||||||
|
import TopicMessagesContext, {
|
||||||
|
ContextProps,
|
||||||
|
} from 'components/contexts/TopicMessagesContext';
|
||||||
|
|
||||||
|
describe('MessagesTable', () => {
|
||||||
|
const searchParams = new URLSearchParams(
|
||||||
|
`?filterQueryType=STRING_CONTAINS&attempt=0&limit=100&seekDirection=${SeekDirection.FORWARD}&seekType=${SeekType.OFFSET}&seekTo=0::9`
|
||||||
|
);
|
||||||
|
const contextValue: ContextProps = {
|
||||||
|
isLive: false,
|
||||||
|
seekDirection: SeekDirection.FORWARD,
|
||||||
|
searchParams,
|
||||||
|
changeSeekDirection: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const setUpComponent = (
|
||||||
|
params: URLSearchParams = searchParams,
|
||||||
|
ctx: ContextProps = contextValue
|
||||||
|
) => {
|
||||||
|
const history = createMemoryHistory();
|
||||||
|
history.push({
|
||||||
|
search: params.toString(),
|
||||||
|
});
|
||||||
|
return render(
|
||||||
|
<Router history={history}>
|
||||||
|
<TopicMessagesContext.Provider value={ctx}>
|
||||||
|
<MessagesTable />
|
||||||
|
</TopicMessagesContext.Provider>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Default props Setup for MessagesTable component', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setUpComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the render', () => {
|
||||||
|
expect(screen.getByRole('table')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the if no elements is rendered in the table', () => {
|
||||||
|
expect(screen.getByText(/No messages found/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check if next button exist and check the click after next click', () => {
|
||||||
|
const nextBtnElement = screen.getByText(/next/i);
|
||||||
|
expect(nextBtnElement).toBeInTheDocument();
|
||||||
|
userEvent.click(nextBtnElement);
|
||||||
|
expect(screen.getByText(/No messages found/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Custom Setup with different props value', () => {
|
||||||
|
it('should check if next click is gone during isLive Param', () => {
|
||||||
|
setUpComponent(searchParams, { ...contextValue, isLive: true });
|
||||||
|
expect(screen.queryByText(/next/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check the display of the loader element', () => {
|
||||||
|
setUpComponent(searchParams, { ...contextValue, isLive: true });
|
||||||
|
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { SeekDirection } from 'generated-sources';
|
||||||
|
|
||||||
|
export interface ContextProps {
|
||||||
|
seekDirection: SeekDirection;
|
||||||
|
searchParams: URLSearchParams;
|
||||||
|
changeSeekDirection(val: string): void;
|
||||||
|
isLive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopicMessagesContext = React.createContext<ContextProps>(
|
||||||
|
{} as ContextProps
|
||||||
|
);
|
||||||
|
|
||||||
|
export default TopicMessagesContext;
|
|
@ -101,3 +101,23 @@ const customRender = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export { customRender as render };
|
export { customRender as render };
|
||||||
|
|
||||||
|
export class EventSourceMock {
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
close: () => void;
|
||||||
|
|
||||||
|
open: () => void;
|
||||||
|
|
||||||
|
error: () => void;
|
||||||
|
|
||||||
|
onmessage: () => void;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
this.open = jest.fn();
|
||||||
|
this.error = jest.fn();
|
||||||
|
this.onmessage = jest.fn();
|
||||||
|
this.close = jest.fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ const reducer = (state = initialState, action: Action): TopicMessagesState => {
|
||||||
case getType(actions.addTopicMessage): {
|
case getType(actions.addTopicMessage): {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
messages: [...state.messages, action.payload],
|
messages: [action.payload, ...state.messages],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case getType(actions.resetTopicMessages):
|
case getType(actions.resetTopicMessages):
|
||||||
|
|
Loading…
Add table
Reference in a new issue