mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-10 02:07:26 +03:00
Finished migrating visits helpers to TS
This commit is contained in:
parent
8a146021dd
commit
260ed3041a
7 changed files with 130 additions and 118 deletions
|
@ -47,15 +47,15 @@ const generateGraphData = (
|
||||||
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
|
borderColor: isBarChart ? 'rgba(70, 150, 229, 1)' : 'white',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
(highlightedData && {
|
highlightedData && {
|
||||||
title,
|
title,
|
||||||
label: highlightedLabel ?? 'Selected',
|
label: highlightedLabel ?? 'Selected',
|
||||||
data: highlightedData,
|
data: highlightedData,
|
||||||
backgroundColor: 'rgba(247, 127, 40, 0.4)',
|
backgroundColor: 'rgba(247, 127, 40, 0.4)',
|
||||||
borderColor: '#F77F28',
|
borderColor: '#F77F28',
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
}) as unknown as ChartDataSets,
|
},
|
||||||
].filter(Boolean),
|
].filter(Boolean) as ChartDataSets[],
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label;
|
const dropLabelIfHidden = (label: string) => label.startsWith('hidden') ? '' : label;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
|
@ -12,35 +11,38 @@ import {
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import { always, cond, reverse } from 'ramda';
|
import { always, cond, reverse } from 'ramda';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { VisitType } from '../types';
|
import { ChartData, ChartDataSets } from 'chart.js';
|
||||||
|
import { Stats, Visit } from '../types';
|
||||||
import { fillTheGaps } from '../../utils/helpers/visits';
|
import { fillTheGaps } from '../../utils/helpers/visits';
|
||||||
import './LineChartCard.scss';
|
|
||||||
import { useToggle } from '../../utils/helpers/hooks';
|
import { useToggle } from '../../utils/helpers/hooks';
|
||||||
import { rangeOf } from '../../utils/utils';
|
import { rangeOf } from '../../utils/utils';
|
||||||
import ToggleSwitch from '../../utils/ToggleSwitch';
|
import ToggleSwitch from '../../utils/ToggleSwitch';
|
||||||
|
import './LineChartCard.scss';
|
||||||
|
|
||||||
const propTypes = {
|
interface LineChartCardProps {
|
||||||
title: PropTypes.string,
|
title: string;
|
||||||
highlightedLabel: PropTypes.string,
|
highlightedLabel?: string;
|
||||||
visits: PropTypes.arrayOf(VisitType),
|
visits: Visit[];
|
||||||
highlightedVisits: PropTypes.arrayOf(VisitType),
|
highlightedVisits: Visit[];
|
||||||
};
|
}
|
||||||
|
|
||||||
const STEPS_MAP = {
|
type Step = 'monthly' | 'weekly' | 'daily' | 'hourly';
|
||||||
|
|
||||||
|
const STEPS_MAP: Record<Step, string> = {
|
||||||
monthly: 'Month',
|
monthly: 'Month',
|
||||||
weekly: 'Week',
|
weekly: 'Week',
|
||||||
daily: 'Day',
|
daily: 'Day',
|
||||||
hourly: 'Hour',
|
hourly: 'Hour',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STEP_TO_DATE_UNIT_MAP = {
|
const STEP_TO_DATE_UNIT_MAP: Record<Step, moment.unitOfTime.Diff> = {
|
||||||
hourly: 'hour',
|
hourly: 'hour',
|
||||||
daily: 'day',
|
daily: 'day',
|
||||||
weekly: 'week',
|
weekly: 'week',
|
||||||
monthly: 'month',
|
monthly: 'month',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STEP_TO_DATE_FORMAT = {
|
const STEP_TO_DATE_FORMAT: Record<Step, (date: moment.Moment | string) => string> = {
|
||||||
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'),
|
hourly: (date) => moment(date).format('YYYY-MM-DD HH:00'),
|
||||||
daily: (date) => moment(date).format('YYYY-MM-DD'),
|
daily: (date) => moment(date).format('YYYY-MM-DD'),
|
||||||
weekly(date) {
|
weekly(date) {
|
||||||
|
@ -52,19 +54,19 @@ const STEP_TO_DATE_FORMAT = {
|
||||||
monthly: (date) => moment(date).format('YYYY-MM'),
|
monthly: (date) => moment(date).format('YYYY-MM'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const determineInitialStep = (oldestVisitDate) => {
|
const determineInitialStep = (oldestVisitDate: string): Step => {
|
||||||
const now = moment();
|
const now = moment();
|
||||||
const oldestDate = moment(oldestVisitDate);
|
const oldestDate = moment(oldestVisitDate);
|
||||||
const matcher = cond([
|
const matcher = cond<never, Step | undefined>([
|
||||||
[ () => now.diff(oldestDate, 'day') <= 2, always('hourly') ], // Less than 2 days
|
[ () => now.diff(oldestDate, 'day') <= 2, always<Step>('hourly') ], // Less than 2 days
|
||||||
[ () => now.diff(oldestDate, 'month') <= 1, always('daily') ], // Between 2 days and 1 month
|
[ () => now.diff(oldestDate, 'month') <= 1, always<Step>('daily') ], // Between 2 days and 1 month
|
||||||
[ () => now.diff(oldestDate, 'month') <= 6, always('weekly') ], // Between 1 and 6 months
|
[ () => now.diff(oldestDate, 'month') <= 6, always<Step>('weekly') ], // Between 1 and 6 months
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return matcher() || 'monthly';
|
return matcher() ?? 'monthly';
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => {
|
const groupVisitsByStep = (step: Step, visits: Visit[]): Stats => visits.reduce<Stats>((acc, visit) => {
|
||||||
const key = STEP_TO_DATE_FORMAT[step](visit.date);
|
const key = STEP_TO_DATE_FORMAT[step](visit.date);
|
||||||
|
|
||||||
acc[key] = acc[key] ? acc[key] + 1 : 1;
|
acc[key] = acc[key] ? acc[key] + 1 : 1;
|
||||||
|
@ -72,7 +74,7 @@ const groupVisitsByStep = (step, visits) => visits.reduce((acc, visit) => {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const generateLabels = (step, visits) => {
|
const generateLabels = (step: Step, visits: Visit[]): string[] => {
|
||||||
const unit = STEP_TO_DATE_UNIT_MAP[step];
|
const unit = STEP_TO_DATE_UNIT_MAP[step];
|
||||||
const formatter = STEP_TO_DATE_FORMAT[step];
|
const formatter = STEP_TO_DATE_FORMAT[step];
|
||||||
const newerDate = moment(visits[0].date);
|
const newerDate = moment(visits[0].date);
|
||||||
|
@ -85,9 +87,14 @@ const generateLabels = (step, visits) => {
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateLabelsAndGroupedVisits = (visits, groupedVisitsWithGaps, step, skipNoElements) => {
|
const generateLabelsAndGroupedVisits = (
|
||||||
|
visits: Visit[],
|
||||||
|
groupedVisitsWithGaps: Stats,
|
||||||
|
step: Step,
|
||||||
|
skipNoElements: boolean,
|
||||||
|
): [string[], number[]] => {
|
||||||
if (skipNoElements) {
|
if (skipNoElements) {
|
||||||
return [ Object.keys(groupedVisitsWithGaps), groupedVisitsWithGaps ];
|
return [ Object.keys(groupedVisitsWithGaps), Object.values(groupedVisitsWithGaps) ];
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = generateLabels(step, visits);
|
const labels = generateLabels(step, visits);
|
||||||
|
@ -95,17 +102,17 @@ const generateLabelsAndGroupedVisits = (visits, groupedVisitsWithGaps, step, ski
|
||||||
return [ labels, fillTheGaps(groupedVisitsWithGaps, labels) ];
|
return [ labels, fillTheGaps(groupedVisitsWithGaps, labels) ];
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateDataset = (stats, label, color) => ({
|
const generateDataset = (data: number[], label: string, color: string): ChartDataSets => ({
|
||||||
label,
|
label,
|
||||||
data: Object.values(stats),
|
data,
|
||||||
fill: false,
|
fill: false,
|
||||||
lineTension: 0.2,
|
lineTension: 0.2,
|
||||||
borderColor: color,
|
borderColor: color,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
});
|
});
|
||||||
|
|
||||||
const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'Selected' }) => {
|
const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'Selected' }: LineChartCardProps) => {
|
||||||
const [ step, setStep ] = useState(
|
const [ step, setStep ] = useState<Step>(
|
||||||
visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly',
|
visits.length > 0 ? determineInitialStep(visits[visits.length - 1].date) : 'monthly',
|
||||||
);
|
);
|
||||||
const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true);
|
const [ skipNoVisits, toggleSkipNoVisits ] = useToggle(true);
|
||||||
|
@ -120,12 +127,12 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
|
||||||
[ highlightedVisits, step, labels ],
|
[ highlightedVisits, step, labels ],
|
||||||
);
|
);
|
||||||
|
|
||||||
const data = {
|
const data: ChartData = {
|
||||||
labels,
|
labels,
|
||||||
datasets: [
|
datasets: [
|
||||||
generateDataset(groupedVisits, 'Visits', '#4696e5'),
|
generateDataset(groupedVisits, 'Visits', '#4696e5'),
|
||||||
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, '#F77F28'),
|
highlightedVisits.length > 0 && generateDataset(groupedHighlighted, highlightedLabel, '#F77F28'),
|
||||||
].filter(Boolean),
|
].filter(Boolean) as ChartDataSets[],
|
||||||
};
|
};
|
||||||
const options = {
|
const options = {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
@ -159,7 +166,7 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
|
||||||
</DropdownToggle>
|
</DropdownToggle>
|
||||||
<DropdownMenu right>
|
<DropdownMenu right>
|
||||||
{Object.entries(STEPS_MAP).map(([ value, menuText ]) => (
|
{Object.entries(STEPS_MAP).map(([ value, menuText ]) => (
|
||||||
<DropdownItem key={value} active={step === value} onClick={() => setStep(value)}>
|
<DropdownItem key={value} active={step === value} onClick={() => setStep(value as Step)}>
|
||||||
{menuText}
|
{menuText}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
|
@ -179,6 +186,4 @@ const LineChartCard = ({ title, visits, highlightedVisits, highlightedLabel = 'S
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LineChartCard.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default LineChartCard;
|
export default LineChartCard;
|
|
@ -1,27 +1,23 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { fromPairs, pipe, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
|
||||||
import { fromPairs, head, keys, pipe, prop, reverse, sortBy, splitEvery, toLower, toPairs, type, zipObj } from 'ramda';
|
|
||||||
import SortingDropdown from '../../utils/SortingDropdown';
|
import SortingDropdown from '../../utils/SortingDropdown';
|
||||||
import PaginationDropdown from '../../utils/PaginationDropdown';
|
import PaginationDropdown from '../../utils/PaginationDropdown';
|
||||||
import { rangeOf } from '../../utils/utils';
|
import { OrderDir, rangeOf } from '../../utils/utils';
|
||||||
import { roundTen } from '../../utils/helpers/numbers';
|
import { roundTen } from '../../utils/helpers/numbers';
|
||||||
import SimplePaginator from '../../common/SimplePaginator';
|
import SimplePaginator from '../../common/SimplePaginator';
|
||||||
|
import { Stats, StatsRow } from '../types';
|
||||||
import GraphCard from './GraphCard';
|
import GraphCard from './GraphCard';
|
||||||
|
import { DefaultChartProps } from './DefaultChart';
|
||||||
|
|
||||||
const propTypes = {
|
const toLowerIfString = (value: any) => type(value) === 'String' ? toLower(value) : value;
|
||||||
stats: PropTypes.object.isRequired,
|
const pickKeyFromPair = ([ key ]: StatsRow) => key;
|
||||||
highlightedStats: PropTypes.object,
|
const pickValueFromPair = ([ , value ]: StatsRow) => value;
|
||||||
highlightedLabel: PropTypes.string,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
sortingItems: PropTypes.object.isRequired,
|
|
||||||
extraHeaderContent: PropTypes.func,
|
|
||||||
withPagination: PropTypes.bool,
|
|
||||||
onClick: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
const toLowerIfString = (value) => type(value) === 'String' ? toLower(value) : value;
|
interface SortableBarGraphProps extends DefaultChartProps {
|
||||||
const pickKeyFromPair = ([ key ]) => key;
|
sortingItems: Record<string, string>;
|
||||||
const pickValueFromPair = ([ , value ]) => value;
|
withPagination?: boolean;
|
||||||
|
extraHeaderContent?: Function;
|
||||||
|
}
|
||||||
|
|
||||||
const SortableBarGraph = ({
|
const SortableBarGraph = ({
|
||||||
stats,
|
stats,
|
||||||
|
@ -31,19 +27,19 @@ const SortableBarGraph = ({
|
||||||
extraHeaderContent,
|
extraHeaderContent,
|
||||||
withPagination = true,
|
withPagination = true,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}: SortableBarGraphProps) => {
|
||||||
const [ order, setOrder ] = useState({
|
const [ order, setOrder ] = useState<{ orderField?: string; orderDir?: OrderDir }>({
|
||||||
orderField: undefined,
|
orderField: undefined,
|
||||||
orderDir: undefined,
|
orderDir: undefined,
|
||||||
});
|
});
|
||||||
const [ currentPage, setCurrentPage ] = useState(1);
|
const [ currentPage, setCurrentPage ] = useState(1);
|
||||||
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
const [ itemsPerPage, setItemsPerPage ] = useState(50);
|
||||||
|
|
||||||
const getSortedPairsForStats = (stats, sortingItems) => {
|
const getSortedPairsForStats = (stats: Stats, sortingItems: Record<string, string>) => {
|
||||||
const pairs = toPairs(stats);
|
const pairs = toPairs(stats);
|
||||||
const sortedPairs = !order.orderField ? pairs : sortBy(
|
const sortedPairs = !order.orderField ? pairs : sortBy(
|
||||||
pipe(
|
pipe<StatsRow, string | number, string | number>(
|
||||||
prop(order.orderField === head(keys(sortingItems)) ? 0 : 1),
|
order.orderField === Object.keys(sortingItems)[0] ? pickKeyFromPair : pickValueFromPair,
|
||||||
toLowerIfString,
|
toLowerIfString,
|
||||||
),
|
),
|
||||||
pairs,
|
pairs,
|
||||||
|
@ -51,7 +47,21 @@ const SortableBarGraph = ({
|
||||||
|
|
||||||
return !order.orderDir || order.orderDir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
return !order.orderDir || order.orderDir === 'ASC' ? sortedPairs : reverse(sortedPairs);
|
||||||
};
|
};
|
||||||
const determineStats = (stats, highlightedStats, sortingItems) => {
|
const determineCurrentPagePairs = (pages: StatsRow[][]): StatsRow[] => {
|
||||||
|
const page = pages[currentPage - 1];
|
||||||
|
|
||||||
|
if (currentPage < pages.length) {
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstPageLength = pages[0].length;
|
||||||
|
|
||||||
|
// Using the "hidden" key, the chart will just replace the label by an empty string
|
||||||
|
return [ ...page, ...rangeOf(firstPageLength - page.length, (i): StatsRow => [ `hidden_${i}`, 0 ]) ];
|
||||||
|
};
|
||||||
|
const renderPagination = (pagesCount: number) =>
|
||||||
|
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
|
||||||
|
const determineStats = (stats: Stats, highlightedStats: Stats | undefined, sortingItems: Record<string, string>) => {
|
||||||
const sortedPairs = getSortedPairsForStats(stats, sortingItems);
|
const sortedPairs = getSortedPairsForStats(stats, sortingItems);
|
||||||
const sortedKeys = sortedPairs.map(pickKeyFromPair);
|
const sortedKeys = sortedPairs.map(pickKeyFromPair);
|
||||||
// The highlighted stats have to be ordered based on the regular stats, not on its own values
|
// The highlighted stats have to be ordered based on the regular stats, not on its own values
|
||||||
|
@ -76,27 +86,13 @@ const SortableBarGraph = ({
|
||||||
max: roundTen(Math.max(...sortedPairs.map(pickValueFromPair))),
|
max: roundTen(Math.max(...sortedPairs.map(pickValueFromPair))),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const determineCurrentPagePairs = (pages) => {
|
|
||||||
const page = pages[currentPage - 1];
|
|
||||||
|
|
||||||
if (currentPage < pages.length) {
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstPageLength = pages[0].length;
|
|
||||||
|
|
||||||
// Using the "hidden" key, the chart will just replace the label by an empty string
|
|
||||||
return [ ...page, ...rangeOf(firstPageLength - page.length, (i) => [ `hidden_${i}`, 0 ]) ];
|
|
||||||
};
|
|
||||||
const renderPagination = (pagesCount) =>
|
|
||||||
<SimplePaginator currentPage={currentPage} pagesCount={pagesCount} setCurrentPage={setCurrentPage} />;
|
|
||||||
|
|
||||||
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
|
const { currentPageStats, currentPageHighlightedStats, pagination, max } = determineStats(
|
||||||
stats,
|
stats,
|
||||||
highlightedStats && keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
highlightedStats && Object.keys(highlightedStats).length > 0 ? highlightedStats : undefined,
|
||||||
sortingItems,
|
sortingItems,
|
||||||
);
|
);
|
||||||
const activeCities = keys(currentPageStats);
|
const activeCities = Object.keys(currentPageStats);
|
||||||
const computeTitle = () => (
|
const computeTitle = () => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{title}
|
{title}
|
||||||
|
@ -107,16 +103,22 @@ const SortableBarGraph = ({
|
||||||
items={sortingItems}
|
items={sortingItems}
|
||||||
orderField={order.orderField}
|
orderField={order.orderField}
|
||||||
orderDir={order.orderDir}
|
orderDir={order.orderDir}
|
||||||
onChange={(orderField, orderDir) => setOrder({ orderField, orderDir }) || setCurrentPage(1)}
|
onChange={(orderField, orderDir) => {
|
||||||
|
setOrder({ orderField, orderDir });
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{withPagination && keys(stats).length > 50 && (
|
{withPagination && Object.keys(stats).length > 50 && (
|
||||||
<div className="float-right">
|
<div className="float-right">
|
||||||
<PaginationDropdown
|
<PaginationDropdown
|
||||||
toggleClassName="btn-sm p-0 mr-3"
|
toggleClassName="btn-sm p-0 mr-3"
|
||||||
ranges={[ 50, 100, 200, 500 ]}
|
ranges={[ 50, 100, 200, 500 ]}
|
||||||
value={itemsPerPage}
|
value={itemsPerPage}
|
||||||
setValue={(itemsPerPage) => setItemsPerPage(itemsPerPage) || setCurrentPage(1)}
|
setValue={(itemsPerPage) => {
|
||||||
|
setItemsPerPage(itemsPerPage);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -141,6 +143,4 @@ const SortableBarGraph = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
SortableBarGraph.propTypes = propTypes;
|
|
||||||
|
|
||||||
export default SortableBarGraph;
|
export default SortableBarGraph;
|
|
@ -80,6 +80,8 @@ export interface CreateVisit {
|
||||||
|
|
||||||
export type Stats = Record<string, number>;
|
export type Stats = Record<string, number>;
|
||||||
|
|
||||||
|
export type StatsRow = [string, number];
|
||||||
|
|
||||||
export interface CityStats {
|
export interface CityStats {
|
||||||
cityName: string;
|
cityName: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
|
|
@ -1,20 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { CardHeader, DropdownItem } from 'reactstrap';
|
import { CardHeader, DropdownItem } from 'reactstrap';
|
||||||
import { Line } from 'react-chartjs-2';
|
import { Line } from 'react-chartjs-2';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import LineChartCard from '../../../src/visits/helpers/LineChartCard';
|
import LineChartCard from '../../../src/visits/helpers/LineChartCard';
|
||||||
import ToggleSwitch from '../../../src/utils/ToggleSwitch';
|
import ToggleSwitch from '../../../src/utils/ToggleSwitch';
|
||||||
|
import { Visit } from '../../../src/visits/types';
|
||||||
|
|
||||||
describe('<LineChartCard />', () => {
|
describe('<LineChartCard />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const createWrapper = (visits = [], highlightedVisits = []) => {
|
const createWrapper = (visits: Visit[] = [], highlightedVisits: Visit[] = []) => {
|
||||||
wrapper = shallow(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />);
|
wrapper = shallow(<LineChartCard title="Cool title" visits={visits} highlightedVisits={highlightedVisits} />);
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('renders provided title', () => {
|
it('renders provided title', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
@ -32,7 +34,7 @@ describe('<LineChartCard />', () => {
|
||||||
[[{ date: moment().subtract(7, 'month').format() }], 'monthly' ],
|
[[{ date: moment().subtract(7, 'month').format() }], 'monthly' ],
|
||||||
[[{ date: moment().subtract(1, 'year').format() }], 'monthly' ],
|
[[{ date: moment().subtract(1, 'year').format() }], 'monthly' ],
|
||||||
])('renders group menu and selects proper grouping item based on visits dates', (visits, expectedActiveItem) => {
|
])('renders group menu and selects proper grouping item based on visits dates', (visits, expectedActiveItem) => {
|
||||||
const wrapper = createWrapper(visits);
|
const wrapper = createWrapper(visits.map((visit) => Mock.of<Visit>(visit)));
|
||||||
const items = wrapper.find(DropdownItem);
|
const items = wrapper.find(DropdownItem);
|
||||||
|
|
||||||
expect(items).toHaveLength(4);
|
expect(items).toHaveLength(4);
|
||||||
|
@ -73,24 +75,24 @@ describe('<LineChartCard />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
[[{}], [], 1 ],
|
[[ Mock.of<Visit>({}) ], [], 1 ],
|
||||||
[[{}], [{}], 2 ],
|
[[ Mock.of<Visit>({}) ], [ Mock.of<Visit>({}) ], 2 ],
|
||||||
])('renders chart with expected data', (visits, highlightedVisits, expectedLines) => {
|
])('renders chart with expected data', (visits, highlightedVisits, expectedLines) => {
|
||||||
const wrapper = createWrapper(visits, highlightedVisits);
|
const wrapper = createWrapper(visits, highlightedVisits);
|
||||||
const chart = wrapper.find(Line);
|
const chart = wrapper.find(Line);
|
||||||
const { datasets } = chart.prop('data');
|
const { datasets } = chart.prop('data') as any;
|
||||||
|
|
||||||
expect(datasets).toHaveLength(expectedLines);
|
expect(datasets).toHaveLength(expectedLines);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('includes stats for visits with no dates if selected', () => {
|
it('includes stats for visits with no dates if selected', () => {
|
||||||
const wrapper = createWrapper([
|
const wrapper = createWrapper([
|
||||||
{ date: '2016-04-01' },
|
Mock.of<Visit>({ date: '2016-04-01' }),
|
||||||
{ date: '2016-01-01' },
|
Mock.of<Visit>({ date: '2016-01-01' }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(wrapper.find(Line).prop('data').labels).toHaveLength(2);
|
expect((wrapper.find(Line).prop('data') as any).labels).toHaveLength(2);
|
||||||
wrapper.find(ToggleSwitch).simulate('change');
|
wrapper.find(ToggleSwitch).simulate('change');
|
||||||
expect(wrapper.find(Line).prop('data').labels).toHaveLength(4);
|
expect((wrapper.find(Line).prop('data') as any).labels).toHaveLength(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
|
@ -1,14 +1,15 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow, ShallowWrapper } from 'enzyme';
|
||||||
import { keys, range, values } from 'ramda';
|
import { range } from 'ramda';
|
||||||
import SortableBarGraph from '../../../src/visits/helpers/SortableBarGraph';
|
import SortableBarGraph from '../../../src/visits/helpers/SortableBarGraph';
|
||||||
import GraphCard from '../../../src/visits/helpers/GraphCard';
|
import GraphCard from '../../../src/visits/helpers/GraphCard';
|
||||||
import SortingDropdown from '../../../src/utils/SortingDropdown';
|
import SortingDropdown from '../../../src/utils/SortingDropdown';
|
||||||
import PaginationDropdown from '../../../src/utils/PaginationDropdown';
|
import PaginationDropdown from '../../../src/utils/PaginationDropdown';
|
||||||
import { rangeOf } from '../../../src/utils/utils';
|
import { OrderDir, rangeOf } from '../../../src/utils/utils';
|
||||||
|
import { Stats } from '../../../src/visits/types';
|
||||||
|
|
||||||
describe('<SortableBarGraph />', () => {
|
describe('<SortableBarGraph />', () => {
|
||||||
let wrapper;
|
let wrapper: ShallowWrapper;
|
||||||
const sortingItems = {
|
const sortingItems = {
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
amount: 'Amount',
|
amount: 'Amount',
|
||||||
|
@ -30,7 +31,7 @@ describe('<SortableBarGraph />', () => {
|
||||||
return wrapper;
|
return wrapper;
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => wrapper && wrapper.unmount());
|
afterEach(() => wrapper?.unmount());
|
||||||
|
|
||||||
it('renders stats unchanged when no ordering is set', () => {
|
it('renders stats unchanged when no ordering is set', () => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
|
@ -40,19 +41,19 @@ describe('<SortableBarGraph />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renders properly ordered stats when ordering is set', () => {
|
describe('renders properly ordered stats when ordering is set', () => {
|
||||||
let assert;
|
let assert: (sortName: string, sortDir: OrderDir, keys: string[], values: number[], done: Function) => void;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const wrapper = createWrapper();
|
const wrapper = createWrapper();
|
||||||
const dropdown = wrapper.renderProp('title')().find(SortingDropdown);
|
const dropdown = wrapper.renderProp('title' as never)().find(SortingDropdown);
|
||||||
|
|
||||||
assert = (sortName, sortDir, expectedKeys, expectedValues, done) => {
|
assert = (sortName: string, sortDir: OrderDir, keys: string[], values: number[], done: Function) => {
|
||||||
dropdown.prop('onChange')(sortName, sortDir);
|
dropdown.prop('onChange')(sortName, sortDir);
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
const stats = wrapper.find(GraphCard).prop('stats');
|
const stats = wrapper.find(GraphCard).prop('stats');
|
||||||
|
|
||||||
expect(keys(stats)).toEqual(expectedKeys);
|
expect(Object.keys(stats)).toEqual(keys);
|
||||||
expect(values(stats)).toEqual(expectedValues);
|
expect(Object.values(stats)).toEqual(values);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -65,28 +66,28 @@ describe('<SortableBarGraph />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renders properly paginated stats when pagination is set', () => {
|
describe('renders properly paginated stats when pagination is set', () => {
|
||||||
let assert;
|
let assert: (itemsPerPage: number, expectedStats: string[], done: Function) => void;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const wrapper = createWrapper(true, range(1, 159).reduce((accum, value) => {
|
const wrapper = createWrapper(true, range(1, 159).reduce<Stats>((accum, value) => {
|
||||||
accum[`key_${value}`] = value;
|
accum[`key_${value}`] = value;
|
||||||
|
|
||||||
return accum;
|
return accum;
|
||||||
}, {}));
|
}, {}));
|
||||||
const dropdown = wrapper.renderProp('title')().find(PaginationDropdown);
|
const dropdown = wrapper.renderProp('title' as never)().find(PaginationDropdown);
|
||||||
|
|
||||||
assert = (itemsPerPage, expectedStats, done) => {
|
assert = (itemsPerPage: number, expectedStats: string[], done: Function) => {
|
||||||
dropdown.prop('setValue')(itemsPerPage);
|
dropdown.prop('setValue')(itemsPerPage);
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
const stats = wrapper.find(GraphCard).prop('stats');
|
const stats = wrapper.find(GraphCard).prop('stats');
|
||||||
|
|
||||||
expect(keys(stats)).toEqual(expectedStats);
|
expect(Object.keys(stats)).toEqual(expectedStats);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildExpected = (size) => [ 'Foo', 'Bar', ...rangeOf(size - 2, (i) => `key_${i}`) ];
|
const buildExpected = (size: number): string[] => [ 'Foo', 'Bar', ...rangeOf(size - 2, (i) => `key_${i}`) ];
|
||||||
|
|
||||||
it('50 items per page', (done) => assert(50, buildExpected(50), done));
|
it('50 items per page', (done) => assert(50, buildExpected(50), done));
|
||||||
it('100 items per page', (done) => assert(100, buildExpected(100), done));
|
it('100 items per page', (done) => assert(100, buildExpected(100), done));
|
||||||
|
@ -95,7 +96,7 @@ describe('<SortableBarGraph />', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders extra header content', () => {
|
it('renders extra header content', () => {
|
||||||
wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<span>
|
<span>
|
||||||
<SortableBarGraph
|
<SortableBarGraph
|
||||||
title="Foo"
|
title="Foo"
|
|
@ -1,8 +1,10 @@
|
||||||
|
import { Mock } from 'ts-mockery';
|
||||||
import { processStatsFromVisits, normalizeVisits } from '../../../src/visits/services/VisitsParser';
|
import { processStatsFromVisits, normalizeVisits } from '../../../src/visits/services/VisitsParser';
|
||||||
|
import { Visit, VisitsStats } from '../../../src/visits/types';
|
||||||
|
|
||||||
describe('VisitsParser', () => {
|
describe('VisitsParser', () => {
|
||||||
const visits = [
|
const visits: Visit[] = [
|
||||||
{
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
userAgent: 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||||
referer: 'https://google.com',
|
referer: 'https://google.com',
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
|
@ -11,8 +13,8 @@ describe('VisitsParser', () => {
|
||||||
latitude: 123.45,
|
latitude: 123.45,
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0',
|
||||||
referer: 'https://google.com',
|
referer: 'https://google.com',
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
|
@ -21,14 +23,14 @@ describe('VisitsParser', () => {
|
||||||
latitude: 1029,
|
latitude: 1029,
|
||||||
longitude: 6758,
|
longitude: 6758,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
countryName: 'Spain',
|
countryName: 'Spain',
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36',
|
||||||
referer: 'https://m.facebook.com',
|
referer: 'https://m.facebook.com',
|
||||||
visitLocation: {
|
visitLocation: {
|
||||||
|
@ -37,14 +39,14 @@ describe('VisitsParser', () => {
|
||||||
latitude: 123.45,
|
latitude: 123.45,
|
||||||
longitude: -543.21,
|
longitude: -543.21,
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
{
|
Mock.of<Visit>({
|
||||||
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36 OPR/38.0.2220.41',
|
||||||
},
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('processStatsFromVisits', () => {
|
describe('processStatsFromVisits', () => {
|
||||||
let stats;
|
let stats: VisitsStats;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
stats = processStatsFromVisits(normalizeVisits(visits));
|
stats = processStatsFromVisits(normalizeVisits(visits));
|
Loading…
Reference in a new issue