Skip to content

Commit

Permalink
Implementing testing-library
Browse files Browse the repository at this point in the history
  • Loading branch information
anarqz committed Aug 15, 2020
1 parent 10668f1 commit 54e314a
Show file tree
Hide file tree
Showing 15 changed files with 266 additions and 117 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.test.js
*.test.tsx
1 change: 0 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ module.exports = {
'global-require': 'off', // https://eslint.org/docs/rules/global-require
'import/no-dynamic-require': 'off', // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md
'no-inner-declarations': 'off', // https://eslint.org/docs/rules/no-inner-declarations
// New rules
'class-methods-use-this': 'off',
'import/extensions': 'off',
'import/prefer-default-export': 'off',
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.next
coverage
6 changes: 6 additions & 0 deletions .storybook/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ i18n.use(initReactI18next).init({
lng: 'en',
whitelist: ['br', 'en'],
debug: true,
backend: {
loadPath: '../public/i18n/{{lng}}/{{ns}}.json',
},
react: {
wait: true,
},
});

export default i18n;
55 changes: 55 additions & 0 deletions __mocks__/react-i18next.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const React = require('react');
const reactI18next = require('react-i18next');

const hasChildren = node => node && (node.children || (node.props && node.props.children));

const getChildren = node => (node && node.children ? node.children : node.props && node.props.children);

const renderNodes = reactNodes => {
if (typeof reactNodes === 'string') {
return reactNodes;
}

return Object.keys(reactNodes).map((key, i) => {
const child = reactNodes[key];
const isElement = React.isValidElement(child);

if (typeof child === 'string') {
return child;
}
if (hasChildren(child)) {
const inner = renderNodes(getChildren(child));
return React.cloneElement(child, { ...child.props, key: i }, inner);
}
if (typeof child === 'object' && !isElement) {
return Object.keys(child).reduce((str, childKey) => `${str}${child[childKey]}`, '');
}

return child;
});
};

const useMock = [k => k, {}];
useMock.t = k => k;
useMock.i18n = {};

const output = {
// this mock makes sure any components using the translate HoC receive the t function as a prop
Trans: ({ children }) => renderNodes(children),
Translation: ({ children }) => children(k => k, { i18n: {} }),
useTranslation: () => useMock,

// mock if needed
I18nextProvider: reactI18next.I18nextProvider,
initReactI18next: reactI18next.initReactI18next,
setDefaults: reactI18next.setDefaults,
getDefaults: reactI18next.getDefaults,
setI18n: reactI18next.setI18n,
getI18n: reactI18next.getI18n,
withTranslation: () => Component => {
Component.defaultProps = { ...Component.defaultProps, t: (t: string) => `t.${t}` };
return Component;
},
};

module.exports = output;
60 changes: 6 additions & 54 deletions components/Button/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,10 @@
import {
getByLabelText,
getByText,
getByTestId,
queryByTestId,
// Tip: all queries are also exposed on an object
// called "queries" which you could import here as well
waitFor,
} from '@testing-library/dom';
import React from 'react';
// adds special assertions like toHaveTextContent
import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';
import Button, { Sizes } from './Button';

function getExampleDOM() {
// This is just a raw example of setting up some DOM
// that we can interact with. Swap this with your UI
// framework of choice 😉
const div = document.createElement('div');
div.innerHTML = `
<label for="username">Username</label>
<input id="username" />
<button>Print Username</button>
`;
const button = div.querySelector('button');
const input = div.querySelector('input');
button.addEventListener('click', () => {
// let's pretend this is making a server request, so it's async
// (you'd want to mock this imaginary request in your unit tests)...
setTimeout(() => {
const printedUsernameContainer = document.createElement('div');
printedUsernameContainer.innerHTML = `
<div data-testid="printed-username">${input.value}</div>
`;
div.appendChild(printedUsernameContainer);
}, Math.floor(Math.random() * 200));
});
return div;
}

test('examples of some things', async () => {
const famousWomanInHistory = 'Ada Lovelace';
const container = getExampleDOM();

// Get form elements by their label text.
// An error will be thrown if one cannot be found (accessibility FTW!)
const input = getByLabelText(container, 'Username');
input.value = famousWomanInHistory;

// Get elements by their text, just like a real user does.
getByText(container, 'Print Username').click();

await waitFor(() => expect(queryByTestId(container, 'printed-username')).toBeTruthy());

// getByTestId and queryByTestId are an escape hatch to get elements
// by a test id (could also attempt to get this element by its text)
expect(getByTestId(container, 'printed-username')).toHaveTextContent(famousWomanInHistory);
// jest snapshots work great with regular DOM nodes!
expect(container).toMatchSnapshot();
test('Renders Button Component as Big', async () => {
render(<Button label='Primary' size={Sizes.BIG} />);
expect(screen.queryByText(/Primary/)).toBeTruthy();
});
2 changes: 1 addition & 1 deletion components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export interface ButtonProps extends WithTranslation {
size: Sizes;
}

const Button: React.FC<ButtonProps> = ({ label, size }: ButtonProps) => {
export const Button: React.FC<ButtonProps> = ({ t, label, size }: ButtonProps) => {
return <button css={[styles.Button, styles[size]]}>{label}</button>;
};

Expand Down
11 changes: 11 additions & 0 deletions components/GHub/UserCard.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Axios from 'axios';
import { GHUser } from '../../models';

export default class UserCardServices {
public endpoint = 'https://api.github.com/users/';

async getGithubUserByUsername(username: string): Promise<GHUser> {
const response = await Axios.get(`${this.endpoint}${username}`);
return new GHUser(response.data);
}
}
41 changes: 3 additions & 38 deletions components/GHub/UserCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,14 @@ export default {
component: UserCard,
} as Meta;

const user = new GHUser({
login: 'alcmoraes',
id: 2521595,
node_id: 'MDQ6VXNlcjI1MjE1OTU=',
avatar_url: 'https://avatars1.githubusercontent.com/u/2521595?v=4',
gravatar_id: '',
url: 'https://api.github.com/users/alcmoraes',
html_url: 'https://github.com/alcmoraes',
followers_url: 'https://api.github.com/users/alcmoraes/followers',
following_url: 'https://api.github.com/users/alcmoraes/following{/other_user}',
gists_url: 'https://api.github.com/users/alcmoraes/gists{/gist_id}',
starred_url: 'https://api.github.com/users/alcmoraes/starred{/owner}{/repo}',
subscriptions_url: 'https://api.github.com/users/alcmoraes/subscriptions',
organizations_url: 'https://api.github.com/users/alcmoraes/orgs',
repos_url: 'https://api.github.com/users/alcmoraes/repos',
events_url: 'https://api.github.com/users/alcmoraes/events{/privacy}',
received_events_url: 'https://api.github.com/users/alcmoraes/received_events',
type: 'User',
site_admin: false,
name: 'Alexandre',
company: null,
blog: '',
location: 'Florianópolis - SC | Brazil',
email: null,
hireable: true,
bio: null,
twitter_username: null,
public_repos: 23,
public_gists: 10,
followers: 24,
following: 24,
created_at: '2012-10-09T16:49:52Z',
updated_at: '2020-08-12T21:27:33Z',
});

const UserCardTranslated = withTranslation('components/ghub/user-card')(UserCard);

const Template: Story<UserCardProps> = (args: UserCardProps) => <UserCardTranslated {...args} />;

export const Default = Template.bind({});
Default.args = { user };
Default.args = { username: 'alcmoraes' };
Default.argTypes = {
user: {
control: { type: 'object' },
username: {
control: 'text',
},
};
25 changes: 25 additions & 0 deletions components/GHub/UserCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import '@testing-library/jest-dom/extend-expect';
import { render, screen } from '@testing-library/react';
import axios from 'axios';
import UserCard from './UserCard';
import { act } from 'react-dom/test-utils';

const GetUserData = (options: Record<string, unknown>) => ({
id: 'foobar',
avatar_url: 'https://avatars0.githubusercontent.com/u/23042052',
html_url: 'https://github.com/alcmoraes',
name: 'Alexandre',
location: 'Florianópolis - SC | Brazil',
...options,
});

jest.mock('axios');

test('Renders UserCard', async () => {
const promise = Promise.resolve({ data: GetUserData({ name: 'Rogério' }) });
(axios.get as jest.Mock).mockImplementationOnce(() => promise);
render(<UserCard username='' />);
await act(() => promise);
expect(screen.queryByText('Rogério')).toHaveClass('MuiTypography-h5');
});
56 changes: 39 additions & 17 deletions components/GHub/UserCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import Card from '@material-ui/core/Card';
import CardActionArea from '@material-ui/core/CardActionArea';
import CardActions from '@material-ui/core/CardActions';
Expand All @@ -10,32 +10,54 @@ import { GHUser } from '../../models';
import styles from './UserCard.style';
import { WithTranslation } from 'next-i18next';
import { withTranslation } from '../../i18n';
import { CircularProgress, Box } from '@material-ui/core';
import UserCardServices from './UserCard.services';

export interface UserCardProps extends WithTranslation {
user: GHUser;
username: string;
}

const UserCard: React.FC<UserCardProps> = ({ t, user }: UserCardProps) => {
return user.id ? (
const UserCard: React.FC<UserCardProps> = ({ t, username }: UserCardProps) => {
const UserCardService = new UserCardServices();
const [user, setUser] = useState(new GHUser());
const [error, setError] = useState(null);

useEffect(() => {
UserCardService.getGithubUserByUsername(username).then(setUser).catch(setError);
}, [username]);

return (
<Card css={styles.Root}>
<CardActionArea>
<CardMedia css={styles.Media} image={user.avatar_url} title={user.name} />
{user.id ? <CardMedia css={styles.Media} image={user.avatar_url} title={user.name} /> : null}
<CardContent>
<Typography gutterBottom variant='h5' component='h2'>
{user.name}
</Typography>
<Typography variant='body2' color='textSecondary' component='p'>
{user.location}
</Typography>
{user.id ? (
<>
<Typography gutterBottom variant='h5' component='h2'>
{user.name}
</Typography>
<Typography variant='body2' color='textSecondary' component='p'>
{user.location}
</Typography>
</>
) : error ? (
<Box textAlign='center'>{error.message}</Box>
) : (
<Box textAlign='center'>
<CircularProgress />
</Box>
)}
</CardContent>
</CardActionArea>
<CardActions>
<Button target='_blank' href={user.html_url} size='small' color='primary'>
{t('go_to_github')}
</Button>
</CardActions>
{user.id ? (
<CardActions>
<Button target='_blank' href={user.html_url} size='small' color='primary'>
{t('go_to_github')}
</Button>
</CardActions>
) : null}
</Card>
) : null;
);
};

export default withTranslation('components/ghub/user-card')(UserCard);
2 changes: 1 addition & 1 deletion models/ghuser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const GHUserRecord = {
};

export default class GHUser extends Record(GHUserRecord) {
constructor(props: any) {
constructor(props?: typeof GHUserRecord) {
props ? super(props) : super();
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"build": "next build",
"start": "node index.js",
"prod": "NODE_ENV=production node index.js",
"test": "concurrently \"jest\"",
"test": "jest",
"test:ci": "CI=true concurrently \"jest\"",
"test:watch": "concurrently \"jest\"",
"type-check": "tsc",
Expand Down Expand Up @@ -62,13 +62,15 @@
"@storybook/preset-typescript": "^3.0.0",
"@storybook/react": "^6.0.5",
"@testing-library/dom": "^7.22.2",
"@testing-library/jest-dom": "^5.11.3",
"@testing-library/react": "^10.4.8",
"@types/classnames": "^2.2.10",
"@types/node": "14.0.13",
"@types/react": "16.9.46",
"@types/react-dom": "16.9.8",
"@typescript-eslint/eslint-plugin": "^3.9.0",
"@typescript-eslint/parser": "3.9.0",
"axios": "^0.19.2",
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"classnames": "^2.2.6",
Expand All @@ -93,4 +95,4 @@
"yarn-deduplicate": "2.0.0"
},
"license": "ISC"
}
}
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
},
"exclude": [
"node_modules",
"components/**/*.stories.tsx"
"components/**/*.stories.tsx",
"components/**/*.test.tsx",
"__mocks__/*.ts"
],
"include": [
"**/*.ts",
Expand Down
Loading

0 comments on commit 54e314a

Please sign in to comment.