More than 1 year has passed since last update.

目的

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

スクリーンショット 2015-07-03 0.20.12.png

テストまわり

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 より高速らしいけど計測して無いのでよくわからない。(比較記事ないのかな?)

参考

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.