实现投票界面
应用的投票界面非常简单:一旦投票启动,它将现实 2 个按钮,分别用来表示 2 个可选项,当投票结束,它显示最终结果。
我们之前都是以测试先行的开发方式,但是在 react 组件开发中我们将先实现组件,再进行测试。这是因为
webpack 和 react-hot-loader 提供了更加优良的 反馈机制 。
而且,也没有比直接看到界面更加好的测试 UI 手段了。
让我们假设有一个 Voting
组件,在之前的入口文件 index.html
的 #app
div 中加载它。由于我们的代码中
包含 JSX 语法,所以需要把 index.js
重命名为 index.jsx
:
//src/index.jsx
import React from 'react';
import Voting from './components/Voting';
const pair = ['Trainspotting', '28 Days Later'];
React.render(
<Voting pair={pair} />,
document.getElementById('app')
);
Voting
组件将使用 pair
属性来加载数据。我们目前可以先硬编码数据,稍后我们将会用真实数据来代替。
组件本身是纯粹的,并且对数据来源并不敏感。
注意,在 webpack.config.js
中的入口点文件名也要修改:
//webpack.config.js
entry: [
'webpack-dev-server/client?http://localhost:8080',
'webpack/hot/only-dev-server',
'./src/index.jsx'
],
如果你此时重启 webpack-dev-server,你将看到缺失 Voting 组件的报错。让我们修复它:
//src/components/Voting.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
render: function() {
return <div className="voting">
{this.getPair().map(entry =>
<button key={entry}>
<h1>{entry}</h1>
</button>
)}
</div>;
}
});
你将会在浏览器上看到组件创建的 2 个按钮。你可以试试修改代码感受一下浏览器自动更新的魅力,没有刷新,
没有页面加载,一切都那么迅雷不及掩耳盗铃。
现在我们来添加第一个单元测试:
//test/components/Voting_spec.jsx
import Voting from '../../src/components/Voting';
describe('Voting', () => {
});
测试组件渲染的按钮,我们必须先看看它的输出是什么。要在单元测试中渲染一个组件,我们需要 react/addons
提供
的辅助函数 renderIntoDocument :
//test/components/Voting_spec.jsx
import React from 'react/addons';
import Voting from '../../src/components/Voting';
const {renderIntoDocument} = React.addons.TestUtils;
describe('Voting', () => {
it('renders a pair of buttons', () => {
const component = renderIntoDocument(
<Voting pair={["Trainspotting", "28 Days Later"]} />
);
});
});
一旦组件渲染完毕,我就可以通过 react 提供的另一个辅助函数 scryRenderedDOMComponentsWithTag
来拿到 button
元素。我们期望存在两个按钮,并且期望按钮的值是我们设置的:
//test/components/Voting_spec.jsx
import React from 'react/addons';
import Voting from '../../src/components/Voting';
import {expect} from 'chai';
const {renderIntoDocument, scryRenderedDOMComponentsWithTag}
= React.addons.TestUtils;
describe('Voting', () => {
it('renders a pair of buttons', () => {
const component = renderIntoDocument(
<Voting pair={["Trainspotting", "28 Days Later"]} />
);
const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
expect(buttons.length).to.equal(2);
expect(buttons[0].getDOMNode().textContent).to.equal('Trainspotting');
expect(buttons[1].getDOMNode().textContent).to.equal('28 Days Later');
});
});
如果我们跑一下测试,将会看到测试通过的提示:
npm run test
当用户点击某个按钮后,组件将会调用回调函数,该函数也由组件的 prop 传递给组件。
让我们完成这一步,我们可以通过使用 React 提供的测试工具 Simulate
来模拟点击操作:
//test/components/Voting_spec.jsx
import React from 'react/addons';
import Voting from '../../src/components/Voting';
import {expect} from 'chai';
const {renderIntoDocument, scryRenderedDOMComponentsWithTag, Simulate}
= React.addons.TestUtils;
describe('Voting', () => {
// ...
it('invokes callback when a button is clicked', () => {
let votedWith;
const vote = (entry) => votedWith = entry;
const component = renderIntoDocument(
<Voting pair={["Trainspotting", "28 Days Later"]}
vote={vote}/>
);
const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
Simulate.click(buttons[0].getDOMNode());
expect(votedWith).to.equal('Trainspotting');
});
});
要想使上面的测试通过很简单,我们只需要让按钮的 onClick
事件调用 vote
并传递选中条目即可:
//src/components/Voting.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
render: function() {
return <div className="voting">
{this.getPair().map(entry =>
<button key={entry}
onClick={() => this.props.vote(entry)}>
<h1>{entry}</h1>
</button>
)}
</div>;
}
});
这就是我们在纯组件中常用的方式:组件不需要做太多,只是回调传入的参数即可。
注意,这里我们又是先写的测试代码,我发现业务代码的测试要比测试 UI 更容易写,所以后面我们会保持这种
方式:UI 测试后行,业务代码测试先行。
一旦用户已经针对某对选项投过票了,我们就不应该允许他们再次投票,难道我们应该在组件内部维护某种状态么?
不,我们需要保证我们的组件是纯粹的,所以我们需要分离这个逻辑,组件需要一个 hasVoted
属性,我们先硬编码
传递给它:
//src/index.jsx
import React from 'react';
import Voting from './components/Voting';
const pair = ['Trainspotting', '28 Days Later'];
React.render(
<Voting pair={pair} hasVoted="Trainspotting" />,
document.getElementById('app')
);
我们可以简单的修改一下组件即可:
//src/components/Voting.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
isDisabled: function() {
return !!this.props.hasVoted;
},
render: function() {
return <div className="voting">
{this.getPair().map(entry =>
<button key={entry}
disabled={this.isDisabled()}
onClick={() => this.props.vote(entry)}>
<h1>{entry}</h1>
</button>
)}
</div>;
}
});
让我们再为按钮添加一个提示,当用户投票完毕后,在选中的项目上添加标识,这样用户就更容易理解:
//src/components/Voting.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
isDisabled: function() {
return !!this.props.hasVoted;
},
hasVotedFor: function(entry) {
return this.props.hasVoted === entry;
},
render: function() {
return <div className="voting">
{this.getPair().map(entry =>
<button key={entry}
disabled={this.isDisabled()}
onClick={() => this.props.vote(entry)}>
<h1>{entry}</h1>
{this.hasVotedFor(entry) ?
<div className="label">Voted</div> :
null}
</button>
)}
</div>;
}
});
投票界面最后要添加的,就是获胜者样式。我们可能需要添加新的 props:
//src/index.jsx
import React from 'react';
import Voting from './components/Voting';
const pair = ['Trainspotting', '28 Days Later'];
React.render(
<Voting pair={pair} winner="Trainspotting" />,
document.getElementById('app')
);
我们再次修改一下组件:
//src/components/Voting.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
isDisabled: function() {
return !!this.props.hasVoted;
},
hasVotedFor: function(entry) {
return this.props.hasVoted === entry;
},
render: function() {
return <div className="voting">
{this.props.winner ?
<div ref="winner">Winner is {this.props.winner}!</div> :
this.getPair().map(entry =>
<button key={entry}
disabled={this.isDisabled()}
onClick={() => this.props.vote(entry)}>
<h1>{entry}</h1>
{this.hasVotedFor(entry) ?
<div className="label">Voted</div> :
null}
</button>
)}
</div>;
}
});
目前我们已经完成了所有要做的,但是 render
函数看着有点丑陋,如果我们可以把胜利界面独立成新的组件
可能会好一些:
//src/components/Winner.jsx
import React from 'react';
export default React.createClass({
render: function() {
return <div className="winner">
Winner is {this.props.winner}!
</div>;
}
});
这样投票组件就会变得很简单,它只需关注投票按钮逻辑即可:
//src/components/Vote.jsx
import React from 'react';
export default React.createClass({
getPair: function() {
return this.props.pair || [];
},
isDisabled: function() {
return !!this.props.hasVoted;
},
hasVotedFor: function(entry) {
return this.props.hasVoted === entry;
},
render: function() {
return <div className="voting">
{this.getPair().map(entry =>
<button key={entry}
disabled={this.isDisabled()}
onClick={() => this.props.vote(entry)}>
<h1>{entry}</h1>
{this.hasVotedFor(entry) ?
<div className="label">Voted</div> :
null}
</button>
)}
</div>;
}
});
最后我们只需要在 Voting
组件做一下判断即可:
//src/components/Voting.jsx
import React from 'react';
import Winner from './Winner';
import Vote from './Vote';
export default React.createClass({
render: function() {
return <div>
{this.props.winner ?
<Winner ref="winner" winner={this.props.winner} /> :
<Vote {...this.props} />}
</div>;
}
});
注意这里我们为胜利组件添加了 ref ,这是因为我们将在单元测试中利用它获取 DOM 节点。
这就是我们的纯组件!注意目前我们还没有实现任何逻辑:我们并没有定义按钮的点击操作。组件只是用来渲染 UI,其它
什么都不需要做。后面当我们将 UI 与 Redux Store 结合时才会涉及到应用逻辑。
继续下一步之前我们要为刚才新增的特性写更多的单元测试代码。首先, hasVoted
属性将会使按钮改变状态:
//test/components/Voting_spec.jsx
it('disables buttons when user has voted', () => {
const component = renderIntoDocument(
<Voting pair={["Trainspotting", "28 Days Later"]}
hasVoted="Trainspotting" />
);
const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
expect(buttons.length).to.equal(2);
expect(buttons[0].getDOMNode().hasAttribute('disabled')).to.equal(true);
expect(buttons[1].getDOMNode().hasAttribute('disabled')).to.equal(true);
});
被 hasVoted
匹配的按钮将显示 Voted
标签:
//test/components/Voting_spec.jsx
it('adds label to the voted entry', () => {
const component = renderIntoDocument(
<Voting pair={["Trainspotting", "28 Days Later"]}
hasVoted="Trainspotting" />
);
const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
expect(buttons[0].getDOMNode().textContent).to.contain('Voted');
});
当获胜者产生,界面将不存在按钮,取而代替的是胜利者元素:
//test/components/Voting_spec.jsx
it('renders just the winner when there is one', () => {
const component = renderIntoDocument(
<Voting winner="Trainspotting" />
);
const buttons = scryRenderedDOMComponentsWithTag(component, 'button');
expect(buttons.length).to.equal(0);
const winner = React.findDOMNode(component.refs.winner);
expect(winner).to.be.ok;
expect(winner.textContent).to.contain('Trainspotting');
});
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论