Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Incremental UI tests #2 #81

Merged
merged 14 commits into from
Jan 10, 2021
7 changes: 7 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,12 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"collectCoverageFrom": [
eshedmargalit marked this conversation as resolved.
Show resolved Hide resolved
"src/**/*.{ts,tsx}",
"!src/index.tsx",
"!src/store.ts"
]
}
}
9 changes: 9 additions & 0 deletions client/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import App from './App';
import { renderWithRouterRedux } from './testUtils/reduxRender';

describe('<App />', () => {
it('renders without crashing', () => {
renderWithRouterRedux(<App />);
eshedmargalit marked this conversation as resolved.
Show resolved Hide resolved
});
});
35 changes: 32 additions & 3 deletions client/src/actions/actions.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { rest } from 'msw';
import { fetchUser } from '.';
import { fetchUser, setReview, updateDraftId, updateDrafts, updateReadingList, updateReviews } from '.';

import { server } from '../mocks/server';
import { blankUser } from '../templates';
import { FETCH_USER } from './actionTypes';
import { blankPaper, blankReview, blankUser } from '../templates';
import {
FETCH_USER,
SET_REVIEW,
UPDATE_DRAFTS,
UPDATE_DRAFT_ID,
UPDATE_READING_LIST,
UPDATE_REVIEWS,
} from './actionTypes';

describe('redux actions', () => {
describe('fetchUser', () => {
Expand All @@ -25,4 +33,25 @@ describe('redux actions', () => {
expect(dispatchMock).toHaveBeenCalledWith({ payload: blankUser, type: FETCH_USER });
});
});

// These are all really simple, but we're chasing that sweet sweet test coverage
describe('other action creators', () => {
const scenarios: {
creator: any;
arg: any;
expectedType: any;
}[] = [
{ creator: setReview, arg: blankReview, expectedType: SET_REVIEW },
{ creator: updateDraftId, arg: 'Mongo ID', expectedType: UPDATE_DRAFT_ID },
{ creator: updateDrafts, arg: [blankReview], expectedType: UPDATE_DRAFTS },
{ creator: updateReadingList, arg: [blankPaper], expectedType: UPDATE_READING_LIST },
{ creator: updateReviews, arg: [blankReview], expectedType: UPDATE_REVIEWS },
];

scenarios.forEach(({ creator, arg, expectedType }) => {
it(`returns the right type for ${creator.name}`, () => {
expect(creator(arg).type).toBe(expectedType);
});
});
});
});
3 changes: 2 additions & 1 deletion client/src/components/DraftPage/DraftPage-redux.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { useProtected } from '../../hooks/useProtected';
import { useProtected } from '../../hooks';

import Drafts from '../Drafts';

function DraftPageRedux(): JSX.Element {
useProtected();

return (
<div className="width80">
<Drafts />
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Home/Home-redux.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { useProtected } from '../../hooks/useProtected';
import { useProtected } from '../../hooks';
import HomeContainer from './Home-container';

export default function HomeRedux(): JSX.Element {
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CalendarOutlined, TeamOutlined } from '@ant-design/icons';
import { Redirect } from 'react-router-dom';
import LazyHero from 'react-lazy-hero';
import { Location } from 'history';
import GoogleButton from 'react-google-button';
import GoogleButton from 'react-google-button/dist/react-google-button';
eshedmargalit marked this conversation as resolved.
Show resolved Hide resolved
import './Login.scss';
import { RootState } from '../../reducers';
import { User } from '../../types';
Expand Down
245 changes: 245 additions & 0 deletions client/src/components/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/* eslint-disable no-console */
/* eslint-disable no-return-assign */
import React from 'react';
import { render, screen } from '@testing-library/react';
import { before } from 'lodash';
import {
getTagColor,
HSLString,
isDOI,
removeMiddleAuthors,
renderCommaSepList,
shortenAuthors,
shortenTableString,
wrapMarkdownWithMath,
} from './utils';

describe('utils', () => {
describe('renderCommaSepList', () => {
it('renders nothing when nothing is passed in', () => {
expect(renderCommaSepList([])).toEqual([]);
});

it('renders one item without a comma separator', () => {
const output = renderCommaSepList(['Shrek']);
render(<div>{output}</div>);
expect(screen.getByText(/Shrek/)).toBeDefined();
});

it('renders two items without a comma separator and an "and"', () => {
const output = renderCommaSepList(['Shrek', 'Donkey']);
render(<div>{output}</div>);
expect(screen.getByText('Shrek')).toBeDefined();
expect(screen.getByText('and Donkey')).toBeDefined();
});

it('renders 3 or more items with comma separation', () => {
const output = renderCommaSepList(['Shrek', 'Donkey', 'Fiona', 'Father Time']);
render(<div>{output}</div>);
expect(screen.getByText(/Shrek,/)).toBeDefined();
expect(screen.getByText(/Donkey,/)).toBeDefined();
expect(screen.getByText(/Fiona/)).toBeDefined();
expect(screen.getByText('and Father Time')).toBeDefined();
});
});

describe('removeMiddleAuthors', () => {
const scenarios: {
description: string;
authorList: string[];
numKeepEitherEnd: number;
expectedResult: string[];
}[] = [
{
description: 'with zero authors',
authorList: [],
numKeepEitherEnd: 0,
expectedResult: [],
},
{
description: 'few authors, keeping many on each end',
authorList: ['Piranesi', 'The Other'],
numKeepEitherEnd: 100,
expectedResult: ['Piranesi', 'The Other'],
},
{
description: 'many authors, keep 0 on either end',
authorList: ['Piranesi', 'The Other'],
numKeepEitherEnd: 0,
expectedResult: ['Piranesi', 'The Other'],
},
{
description: 'many authors, keep 2 on either end',
authorList: ['Piranesi', 'The Other', 'The Biscuit Box Man', 'Sixteen', "Sylvia D'Agostino"],
numKeepEitherEnd: 2,
expectedResult: ['Piranesi', 'The Other', '...', 'Sixteen', "Sylvia D'Agostino"],
},
];

scenarios.forEach(({ description, authorList, numKeepEitherEnd, expectedResult }) => {
it(`returns the correct result ${description}`, () => {
expect(removeMiddleAuthors(authorList, numKeepEitherEnd)).toEqual(expectedResult);
});
});
});

describe('shortenAuthors', () => {
// TODO EM: would this ever happen? Can we revise the functionality and rmeove this test?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

callout for @eshedmargalit

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this can happen and I think we want to allow it. Example: you start writing a draft but haven't added authors yet. It will save the draft with authors: [''], and we want that to render as

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay, we can remove this comment then and keep the test

it('returns NAText when a single blank author is provided', () => {
const output = shortenAuthors(['']);
render(output as JSX.Element);
expect(screen.getByText('N/A')).toBeDefined();
});

it('returns NAText when no authors are provided', () => {
const output = shortenAuthors([]);
render(output as JSX.Element);
expect(screen.getByText('N/A')).toBeDefined();
});

const scenarios: {
description: string;
authors: string[];
expectedOutput: string;
}[] = [
{
description: 'with 2 authors',
authors: ['Stephen Curry', 'Klay Thompson'],
expectedOutput: 'Curry and Thompson',
},
{
description: 'with 1 author',
authors: ['Stephen Curry'],
expectedOutput: 'Stephen Curry',
},
{
description: 'with 3 authors',
authors: ['Stephen Curry', 'Klay Thompson', 'Billy the Shooter'],
expectedOutput: 'Curry et al.',
},
];

scenarios.forEach(({ description, authors, expectedOutput }) => {
it(`returns the right string ${description}`, () => {
expect(shortenAuthors(authors)).toEqual(expectedOutput);
});
});
});

describe('shortenTableString', () => {
const scenarios: {
description: string;
inString: string;
cutoff: number;
expectedText: string;
}[] = [
{
description: 'with no string',
inString: '',
cutoff: -5,
expectedText: 'N/A',
},
{
// TODO EM: is this desired?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, no. Cutoff should be strictly >= 0.

description: 'with a string and a negative cutoff',
inString: 'testing one two',
cutoff: -5,
expectedText: '...',
},
{
description: 'with a string and a small cutoff',
inString: 'testing one two',
cutoff: 5,
expectedText: 'testi...',
},
{
description: 'with a string and a huge cutoff',
inString: 'testing one two',
cutoff: 500,
expectedText: 'testing one two',
},
{
description: 'with a string and no cutoff',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't call this "no cutoff"; I'd say the cutoff is 0.

But I do think the cutoff should have to be a natural number

inString: 'testing one two',
cutoff: 0,
expectedText: '...',
},
];

scenarios.forEach(({ description, inString, cutoff, expectedText }) => {
it(`renders the correct element ${description}`, () => {
render(shortenTableString(inString, cutoff));
expect(screen.getByText(expectedText)).toBeDefined();
});
});
});

describe('getTagColor', () => {
const scenarios: {
description: string;
input: string;
output: HSLString;
}[] = [
{
description: 'with no tag',
input: '',
output: 'hsl(0,80%,30%)',
},
{
description: 'with some tag',
input: 'i am a tag',
output: 'hsl(-354,80%,30%)',
},
];
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why test these scenarios at all? If it's just a functionality test, the next one covers that, and I don't think we actually care what color the blank string is assigned to.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

roger roger, if this isn't important, we can scrap the tests


scenarios.forEach(({ description, input, output }) => {
it(`returns the right HSLString ${description}`, () => {
expect(getTagColor(input)).toEqual(output);
});
});

it('returns the same color for the same tag', () => {
const tag = 'tag';
expect(getTagColor(tag)).toEqual(getTagColor(tag));
});
});

describe.skip('getReviewStats', () => {
// TODO: once review data deserialization is figured out, write this test
});

describe('isDOI', () => {
it('recognizes DOIs starting with 10', () => {
expect(isDOI('10.1.1')).toBeTruthy();
});

it('recognizes DOIs with doi.org', () => {
expect(isDOI(' doi.org ')).toBeTruthy();
});

it('cannot be fooled by mere mortals', () => {
expect(isDOI(' doI.org ')).toBeFalsy();
expect(isDOI('10 .1.1')).toBeFalsy();
});
});

describe('wrapMarkdownWithMath', () => {
// This is silly, but the react-katex library throws an ugly warning: https://github.com/talyssonoc/react-katex/issues/59
// so we'll just suppress it :)
const originalConsoleWarn = console.warn;
beforeAll(() => (console.warn = jest.fn()));
afterAll(() => (console.warn = originalConsoleWarn));

it('does not alter non-math string', () => {
const testString = 'The Year I Named the Constellations';
render(wrapMarkdownWithMath(testString));
expect(screen.getByText(testString)).toBeDefined();
});

it('renders math strings with markdown', () => {
render(wrapMarkdownWithMath('$\\sum_0^\\infty$'));
expect(screen.getAllByText(/∑/)).toBeDefined();
eshedmargalit marked this conversation as resolved.
Show resolved Hide resolved
expect(screen.getAllByText(/∞/)).toBeDefined();
});
});
});
4 changes: 3 additions & 1 deletion client/src/components/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const renderCommaSepList = (items: string[]): JSX.Element[] =>
export const removeMiddleAuthors = (authorList: string[], numKeepEitherEnd: number): string[] => {
const numAuthors = authorList.length;
const numKeepTotal = numKeepEitherEnd * 2;
if (numAuthors <= numKeepTotal) {
if (numAuthors <= numKeepTotal || numKeepTotal <= 2) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I figured that if this is 0, you just want to keep the whole authorlist? Not sure how you'd want to handle that.

return authorList;
}

Expand All @@ -60,6 +60,8 @@ export const removeMiddleAuthors = (authorList: string[], numKeepEitherEnd: numb
};

export const shortenAuthors = (authors: string[]): JSX.Element | string => {
if (!authors.length) return <NAText />;

let authorString = '';

if (authors.length === 2) {
Expand Down
1 change: 1 addition & 0 deletions client/src/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
declare module 'react-lazy-hero';
declare module 'react-katex';
declare module 'react-google-button/dist/react-google-button';
2 changes: 2 additions & 0 deletions client/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './useIsMounted';
export * from './useProtected';
1 change: 1 addition & 0 deletions client/src/hooks/useIsMounted/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useIsMounted';
16 changes: 16 additions & 0 deletions client/src/hooks/useIsMounted/useIsMounted.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { useIsMounted } from '.';

// Because you can't run a hook outside of a react component, we'll make a small test case:
function TestComponent(): JSX.Element {
const isMounted = useIsMounted();
return <h1>{isMounted().toString()}</h1>;
}

xdescribe('useIsMounted', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is skipped because I don't really understand how it works, or how to test it. Any ideas?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea how to test it either, unless you can stall mounting halfway through the test.

But this hook allows you to ask if the component that uses the hook is still mounted

it('returns false before the component is mounted', async () => {
render(<TestComponent />);
expect(screen.getByText(/true/)).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useRef, useEffect } from 'react';
import { useRef, useEffect, useCallback } from 'react';

export const useIsMounted = (): (() => boolean) => {
const mountedRef = useRef(false);
Expand Down