Finished migrating visits helpers to TS

This commit is contained in:
Alejandro Celaya 2020-09-04 18:43:26 +02:00
parent 8a146021dd
commit 260ed3041a
7 changed files with 130 additions and 118 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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);
}); });
}); });

View file

@ -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"

View file

@ -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));