npx create-react-app [app-name] --scripts-version @apptension/react-scripts
npx create-react-app suchar-workshop --scripts-version @apptension/react-scripts
//.env
REACT_APP_BASE_API_URL = 'https://official-joke-api.appspot.com/'yarn plop component buttonyarn plopyarn plop container detailsPageyarn plop module contentRegistryexport const INITIAL_STATE = new Immutable({
jokes: Immutable([]),
});export const { Types: JokesTypes, Creators: JokesActions } = createActions(
{
fetch: [''],
fetchSuccess: ['newJoke'],
},
{ prefix: 'JOKES/' }
);import { put, takeLatest } from 'redux-saga/effects';
import api from '../../shared/services/api';
import { JokesTypes, JokesActions } from './jokes.redux';
export function* fetch() {
const { data } = yield api.get('jokes/programming/random');
yield put(JokesActions.fetchSuccess(data));
}
export function* watchJokes() {
yield takeLatest(JokesTypes.FETCH, fetch);
}
import mockApi from '../../../shared/utils/mockApi';
import { expectSaga } from 'redux-saga-test-plan';
it('should dispatch fetchSuccess action with response from api', async () => {
//will it work?
const data = ["some data from API"];
await expectSaga(watchJokes)
.withState(defaultState)
.put(JokesActions.fetchSuccess(data))
.dispatch(JokesActions.fetch())
.run();
});export default nock('https://official-joke-api.appspot.com/')
.defaultReplyHeaders({
'access-control-allow-origin': '*',
});//jokes.sagas.spec.js
mockApi.get('/jokes/programming/random').reply(200, data);import { createActions, createReducer } from 'reduxsauce';
import Immutable from 'seamless-immutable';
export const { Types: JokesTypes, Creators: JokesActions } = createActions(
{
fetch: [''],
fetchSuccess: ['newJoke'],
},
{ prefix: 'JOKES/' }
);
export const INITIAL_STATE = new Immutable({
jokes: Immutable([]),
});
const fetchSuccess = (state, { newJoke }) =>
state.update('jokes', jokes => jokes.concat(newJoke));
export const reducer = createReducer(INITIAL_STATE, {
[JokesTypes.FETCH_SUCCESS]: fetchSuccess,
});
import { jokes } from '../../../shared/utils/fixtures';
const state = Immutable({
jokes: [],
});
describe('when fetchSuccess action is received', () => {
it('should save new jokes to store', () => {
const action = JokesActions.fetchSuccess([jokes[0], jokes[1]]);
const expectedState = state.set('jokes', [jokes[0], jokes[1]]);
expect(jokesReducer(state, action)).toEqual(expectedState);
});
});
export const apiResponse = {
id: 28,
type: 'programming',
setup: 'To understand what recursion is...',
punchline: 'You must first understand what recursion is',
};
export const jokes = [
{
id: 28,
type: 'programming',
setup: 'To understand what recursion is...',
punchline: 'You must first understand what recursion is',
},
{
id: 1333,
type: 'programming',
setup: 'Why do C# and Java developers keep breaking their keyboards?',
punchline: 'Because they use a strongly typed language.',
},
{
id: 123,
type: 'something else',
setup: 'How do you check if a webpage is HTML5?',
punchline: 'Try it out on Internet Explorer',
},
];import { createSelector } from 'reselect';
import { prop } from 'ramda';
export const selectJokesDomain = prop('jokes');
export const selectRecipes = createSelector(
selectJokesDomain,
prop('jokes')
);
describe('Jokes: selectors', () => {
const defaultState = Immutable({
jokes: [],
});
describe('selectJokes', () => {
it('should select all jokes', () => {
const state = defaultState.set('jokes', [{ id: 1 }, { id: 2 }]);
const expectedSelection = [{ id: 1 }, { id: 2 }];
expect(selectJokes(state)).toEqual(expectedSelection);
});
});
});
const mapStateToProps = createStructuredSelector({
jokes: selectJokes,
});yarn plop container jokesexport const mapDispatchToProps = dispatch =>
bindActionCreators(
{
fetchJoke: JokesActions.fetch,
},
dispatch
);export class Jokes extends PureComponent {
static propTypes = {
fetchJoke: PropTypes.func.isRequired,
jokes: PropTypes.array.isRequired,
};
render() {
return (
<Container>
<h1>
Jokes generator
</h1>
<button onClick={() => this.props.fetchJoke()}>
fetch a joke
</button>
<div>
{this.props.jokes.map(joke => (
<div key={joke.id}>
<p>{joke.setup}</p>
<p>{joke.punchline}</p>
</div>
))}
</div>
</Container>
);
}
}import { defineMessages } from 'react-intl';
export default defineMessages({
pageTitle: {
id: 'home.pageTitle',
defaultMessage: 'Homepage',
},
welcome: {
id: 'home.welcome',
defaultMessage: 'Hello World!',
},
});
<FormattedMessage {...messages.title} />this.props.intl.formatMessage(messages.title);yarn extract-intl// src/translations/en.json
{
"app.pageTitle": "Apptension Boilerplate",
"home.pageTitle": "Homepage",
"home.welcome": "Hello World!",
"notFound.pageTitle": "Not found",
"notFound.title": "Error: 404",
"recipes.fetchButton": "Get recipes",
"recipes.pageTitle": "Recipes",
"unsupported.pageTitle": "Unsupported Browser",
"unsupported.title": "Unsupported Browser"
}
yarn testexpect().toBe()
expect().toEqual()
Using jest assertions / spies
No chai / sinon
describe('Button: Component', () => {
describe('when its clicked', () => {
describe('in active mode', () => {
it('should do something', () => {});
});
describe('in inactive mode', () => {
it('should do something else', () => {});
});
});
});global.$ = jest.fn();
jest.mock('utils/services/firebase', () => ({ ... }));export const jokes = [
{
id: 28,
type: 'programming',
setup: 'To understand what recursion is...',
punchline: 'You must first understand what recursion is',
},
{
id: 1333,
type: 'programming',
setup: 'Why do C# and Java developers keep breaking their keyboards?',
punchline: 'Because they use a strongly typed language.',
},
{
id: 123,
type: 'something else',
setup: 'How do you check if a webpage is HTML5?',
punchline: 'Try it out on Internet Explorer',
},
];const fnMock = jest.fn();
const fnSpy = jest.spyOn(window, 'addEventListener');
jestFn
.mockClear()
.mockReset()
.mockRestore()
.mockImplementation(fn)
.mockReturnValue(value)expect(jestFn)
.toHaveBeenCalled()
.toHaveBeenCalledWith()// all exported functions will be replaced with jest.fn()
jest.mock('../path/to/module');
jest.mock('../path/to/module',
() => 'this function will override default export'
);
jest.mock('../path/to/module', () => ({
fn1: () => 'function 1 mock',
fn2: () => 'function 2 mock',
}));<ThemeProvider theme={{ colors: {primary: red, secondary: blue} }}>
...wholeApp
</ThemeProvider>export const Container = styled.button`
background-color: ${props => props.theme.colors.primary};
`;export const Container = styled.button`
background-color: ${props => props.theme.colors.primary};
`;export const Container = styled.button`
background-color: ${themeColor('primary')};
`;const themeColor = colorName => path(['theme', 'colors', colorName]);//header.component.js
<ThemeProvider theme={{ isShrinked: true }}>
<Logo />
<Menu />
</ThemeProvider>
//header.styles.js
export const Logo = styled.img`
height: ${props => props.theme.mode.isShrinked ? 10 : 20}
`;
export const Menu = styled.img`
color: ${themeColor('primary')};
height: ${props => props.theme.mode.isShrinked ? 10 : 20}
`;//button.stories.js
const themeDecorator = withTheme(theme);
storiesOf('Button', module)
.addDecorator(themeDecorator)
.add('Primary', () => <Button {...defaultProps} />);
storiesOf('Button', module)
.addDecorator(themeDecorator)
.add('Primary wide', () => <Button {...defaultProps} wide />);it('should render correctly', () => {
const wrapper = shallow(component());
expect(wrapper).toMatchSnapshot();
});const wrapper = shallow(<Button/>);
wrapper.debug(); / wrapper.html();
wrapper.simulate('click'); == wrapper.prop('onClick')();
wrapper.find('.selector'); / wrapper.find(StyledComponent);
wrapper.find('multiple elements selector').prop('propName');
==
wrapper.find('multiple elements selector').first().prop('propName');
// find returns all matching elements,
// but if method invoked on the result expects only one node,
// it will use the first matching element
wrapper.dive(); // return shallow wrapper of the first child of current wrapper
wrapper.update(); // force update
wrapper.prop('name', 'value');
wrapper.unmount();yarn eject