目的
ava + jsdom がどういうものかさらっと触れてみる。
ついでに以前書いた ReactJS + facebook/flux を ES6 の記述でカウンターのサンプル をリファクタしてみた。
ファイル構成
.
├── index.html
├── js
│ └── bundle.js
├── package.json
├── src
│ ├── app.jsx
│ └── app_dispatcher.js
└── test
├── hoge.html
└── hoge_test.js
package
pacage.json
{
"name": "ava_and_jsdom",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "browserify src/app.jsx -o js/bundle.js --debug",
"watch": "watchify src/app.jsx -o js/bundle.js -v --debug",
"test": "ava"
},
"browserify": {
"transform": [
[
"babelify",
{
"presets": [
"es2015",
"react"
]
}
]
]
},
"author": "Takashi ITO",
"license": "MIT",
"devDependencies": {
"ava": "^0.10.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-react": "^6.3.13",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"delay": "^1.3.1",
"jsdom": "^7.2.2",
"watchify": "^3.7.0"
},
"dependencies": {
"flux": "^2.1.1",
"react": "^0.14.6",
"react-dom": "^0.14.6"
}
}
インストールとかビルドとか
$ npm install
$ npm run watch (or npm run build)
コード
src/app.jsx
'use strict';
import AppDispatcher from './app_dispatcher';
import React from 'react';
import ReactDOM from 'react-dom';
import {EventEmitter} from 'events';
// Store
class CounterStore extends EventEmitter {
constructor() {
super();
AppDispatcher.register(this._onAction.bind(this));
this.CHANGE_EVENT = 'change';
this.counter = 0;
}
_onAction(action) {
switch(action.actionType) {
case 'COUNTER/UPDATE':
this.counter += action.num;
this.emitChange();
break;
}
}
emitChange() {
this.emit(this.CHANGE_EVENT);
}
addChangeListener(callback) {
this.on(this.CHANGE_EVENT, callback);
}
removeChangeListener(callback) {
this.removeListener(this.CHANGE_EVENT, callback);
}
getState() {
return {count: this.counter};
}
}
const counterStore = new CounterStore();
// Action creators
const CounterActions = {
update: function(num) {
AppDispatcher.dispatch({
actionType: 'COUNTER/UPDATE',
num: num
});
}
};
// Component
class CounterBtnComponent extends React.Component {
render() {
return (
<div>
<div>
<button onClick={this.onClick.bind(this)}>+1</button>
<button onClick={this.onClick.bind(this)}>-1</button>
</div>
</div>
);
}
onClick(event) {
CounterActions.update(Number(event.target.textContent));
}
}
class AppComponent extends React.Component {
constructor() {
super();
this.state = counterStore.getState();
}
componentDidMount() {
counterStore.addChangeListener(this._onChange.bind(this));
}
componentWillUnmount() {
counterStore.removeChangeListener(this._onChange.bind(this));
}
render() {
return (
<div>
<span>count: {this.state.count}</span>
<CounterBtnComponent />
</div>
);
}
_onChange() {
this.setState(counterStore.getState());
}
}
ReactDOM.render(
<AppComponent />,
document.querySelector('.js-container')
);
src/app_dispatcher.js
var Dispatcher = require('flux').Dispatcher;
module.exports = new Dispatcher();
index.html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>React+ava+jsdom</title>
</head>
<body>
<div class="js-container"></div>
<script src="js/bundle.js"></script>
</body>
</html>
表示
$ python -m SimpleHTTPServer 8000
テストまわり
test/hoge.html
<div class="js-container"></div>
test/hoge_test.js
'use strict';
import test from 'ava';
import jsdom from 'jsdom';
import fs from 'fs';
const hoge_html = fs.readFileSync('hoge.html', 'utf-8');
const dom = {
then: (onFulfill, onReject) => {
const config = {
html: hoge_html,
scripts: ['../js/bundle.js'],
done: (err, window) => {
if (err) {
onReject(err);
} else {
onFulfill(window);
}
}
};
jsdom.env(config);
}
};
const SELECTOR = {
counterNum: 'body > div.js-container > div > span > span:nth-child(2)',
plusBtn: 'body > div.js-container > div > div > div > button:nth-child(1)',
minusBtn: 'body > div.js-container > div > div > div > button:nth-child(2)'
};
test('initial counter num', t => {
return Promise.resolve(dom)
.then((window) => {
let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
t.is(counterNum, '0');
});
});
test('1 increases when click +1 button', t => {
return Promise.resolve(dom)
.then((window) => {
let plusBtn = window.document.querySelector(SELECTOR.plusBtn);
plusBtn.click();
return window;
})
.then((window) => {
let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
t.is(counterNum, '1');
});
});
test('1 decreases when click -1 button', t => {
return Promise.resolve(dom)
.then((window) => {
let plusBtn = window.document.querySelector(SELECTOR.minusBtn);
plusBtn.click();
return window;
})
.then((window) => {
let counterNum = window.document.querySelector(SELECTOR.counterNum).textContent;
t.is(counterNum, '-1');
});
});
ava のアサーション
- .pass([message])
- .fail([message])
- .ok(value, [message])
- .notOk(value, [message])
- .true(value, [message])
- .false(value, [message])
- .is(value, expected, [message])
- .not(value, expected, [message])
- .same(value, expected, [message])
- .notSame(value, expected, [message])
- .throws(function|promise, error, [message])
- .doesNotThrow(function|promise, [message])
- .ifError(error, [message])
テスト実行
$ npm test
> sample@1.0.0 test /Users/username/sample/
> ava
3 passed
感想
- 想定してたより書きやすそうかも。
- ava は mocha より高速らしいけど計測して無いのでよくわからない。(比較記事ないのかな?)