The Speaker listing component

Another piece of the application that we can test is the components. There are two types of component in a typical React + Redux application. We have a container and presentational components.

Container components don't typically hold any real HTML in them. The render function for a container component simply references a single presentational component.

Presentational components don't typically have any business logic in them. They receive properties and display those properties.

In our journey from the back-end to the front-end, we have been covering the retrieval and updating of data. Next, let's look at the container component that will use this data.

Our container component is going to be a simple one. Let's start with the typical existence test.

import { expect } from 'chai';
import { SpeakersPage } from './SpeakersPage';

describe('Speakers Page', () => {
it('exists', () => {
expect(SpeakersPage).to.exist;
});
});

Simple and straightforward; now to make it pass.

export class SpeakersPage {
}

Next is the render function of the component.

import React from 'react';
import Enzyme, { mount, shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { SpeakersPage } from './SpeakersPage';

describe('Render', () => {
beforeEach(() => {
Enzyme.configure({ adapter: new Adapter() });
});

it('renders', () => {
// arrange
const props = {
speakers: [{
id: 'test-speaker',
firstName:'Test',
lastName:'Speaker'
}]
};

// act
const component = shallow(<SpeakersPage { ...props } />);

// assert
expect(component.find('SpeakerList')).to.exist;
expect(component.find('SpeakerList').props().speakers)
.to.deep.equal(props.speakers);
});
});

This test introduces some new concepts. Starting at the act portion of the test. We are using Enzyme's shallow render. A shallow render will render the React component but not the component's children. In this case, we are expecting that a SpeakerList component exists and that this component is rendering it. The Enzyme adapter configuration is shown here, but it can also be moved to test.js after the tests pass.

We are also checking the props to make sure we pass the speakers into the presentational component. To make this test pass, we must make modifications to the SpeakersPage component, but we must also create a SpeakerList component. Let's do that now.

export class SpeakersPage extends Component {
render() {
return (
<SpeakerList speakers={this.props.speakers} />
);
}
}

And then in a new file, we need to add the SpeakerList.

export const SpeakerList = ({speakers}) => {}

You may have noticed that our container component doesn't have any logic. In fact, all it does is render the SpeakerList component. If that is all it does, why is it a container component? The reason is that this component is going to be a Redux-connected component. We want to keep the Redux code in our business logic and out of our display components. So, we are treating this as a higher order component and just using it to pass data through to the presentational components. Later, when we get to the speaker detail component you will see a container component with a little business logic.

For now, our SpeakerList component looks a little anemic and doesn't really work as part of a React Redux app. Time to test our presentational components.

describe('Speaker List', () => {
it('exists', () => {
expect(SpeakerList).to.exist;
});
});

Because of the last test, this test will automatically pass. Normally we would not write this test if we followed to progression what we just did. In reality, what we should have done is ignore the previous test, create this test, and then create the SpeakerList component. After which, we could have re-enabled the previous test and gotten it to pass.

The next step is to test that a message of no speakers available is rendered when the speakers array is empty.

function setup(props) {
const componentProps = {
speakers: props.speakers || []
};

return shallow(<SpeakerList {...componentProps} />);
}

describe('On Render', () => {
describe('No Speakers Exist', () => {
it('renders no speakers message', () => {
// arrange
const speakers = [];

// arrange
const component = setup({speakers});

// assert
expect(component.find('#no-speakers').text())
.to.equal('No Speakers Available.');
});
});
});

For this test, we created a helper function to initialize the component with the props that we need. To make the test pass we just need to return a div with the correct text.

export const SpeakerList = ({speakers}) => {
return (
<div>
<h1>Speakers</h1>
<div id="no-speakers">No Speakers Available.</div>
</div>
);
};

While we are only testing for the no-speakers div, we can have decoration that we decide not to test. In this case, we want a header on the page. Our tests should pass regardless.

So, now we are ready to test for when speakers do exist.

describe('Speakers Exist', () => {
it('renders a table when speakers exist', () => {
// arrange
const speakers = [{
id: 'test-speaker-1',
firstName: 'Test',
lastName: 'Speaker 1'
}, {
id: 'test-speaker-2',
firstName: 'Test',
lastName: 'Speaker 2'
}];

// act
const component = setup({speakers});

// assert
expect(component.find('.speakers')
.children()).to.have.lengthOf(2);
expect(component.find('.speakers')
.childAt(0).type().name).to.equal('SpeakerListRow');
});
});

In this test, we check for two things. We want the correct number of speaker rows to display and we want them to be rendered by a new SpeakerListRow component.

export const SpeakerList = ({speakers}) => {
let contents = <div>Error!</div>;

if(speakers.length === 0) {
contents = <div id="no-speakers">No Speakers Available.</div>;
} else {
contents = (
<table className="table">
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody className="speakers">
{
speakers.map(speaker =>
<SpeakerListRow key={speaker.id} speaker={speaker} />)
}
</tbody>
</table>
);
}

return (
<div>
<h1>Speakers</h1>
{ contents }
</div>
);
};

The component code has changed significantly because of our latest test. We had to add some logic, and we also added a default error case if somehow the content were to make it through without being assigned.

There is one more component to make the code work correctly for this section. We are not going to test that component in this book, though. The component has no logic inside it and is left as an exercise to you to create.

In order to create that component, it would be nice if the application ran. Right now, we have not wired up Redux so the application won't render anything. Let's walk through the configuration we are using for Redux now.

Inside index.js, we need to add a few items to let Redux work. Your index should look similar to this:

 import React from 'react';
import ReactDOM from 'react-dom';
import {BrowserRouter} from 'react-router-dom';
import {Provider} from 'react-redux';
import registerServiceWorker from './registerServiceWorker';
import configureStore from './store/configureStore';
import { getSpeakers } from './actions/speakerActions';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './components/App.js';

const store = configureStore();
store.dispatch(getSpeakers());

ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App/>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);

registerServiceWorker();

The two parts that we have added are the Redux store including an initial call to dispatch the load speakers action, and markup to add the Redux provider.

Where your other routes are defined, you will need to add routes for the speaker section. We are placing the Routes in App.js.

<Route exact path='/speakers/:id' component={SpeakerDetailPage}/>
<Route exact path='/speakers' component={SpeakersPage}/>

Lastly, we have to convert our component to a Redux component. Add the following lines to the bottom of your speaker's page component file.

import { connect } from 'react-redux';

function mapStateToProps(state) {
return {
speakers: state.speakers
};
}

function mapDispatchToProps(dispatch) {
return bindActionCreators(speakerActions, dispatch);
}

export default connect(mapStateToProps, mapDispatchToProps)(SpeakersPage);

Starting at the bottom of the code sample, the connect function is provided by Redux and will wire up all the Redux functionality into our component. The two functions passed in, mapStateToProps and mapDispatchToProps, are passed in as a way to populate state and provide actions for our component to execute.

Inside mapDispatchToProps we are calling bindActionCreators; this is another Redux-provided function and will give us an object containing all the actions. By returning that object directly from mapDispatchToProps, we are adding the actions directly to props. We could also create our own object containing an actions property and then assign the result of the bindActionCreators to that property.

Anywhere inside the application that references SpeakersPage can now be changed to just SpeakersPage, which will grab our new default export. Do not make this change in the tests. Inside the tests we still want the named import.

With those things done, we should be able to run the application and navigate to the speakers route. If you have not added a link to the speakers route, now would be a good time so that you don't have to type the route directly in the URL every time.

Once you arrive at the speakers route, you should see that there are no speakers and we receive our message. We need some way to populate the speakers so that we can test the listing. We will cover a way to populate speakers in the next section. For now, in the mock API modify the constructor to contain a couple of speakers. Modifying the service in this way will cause a few tests to break, so after you have visually verified that everything is looking good, be sure to remove or at least comment out the code you added.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.112.56