JavaScript


Kazuhito Hokamura

2013/02/23

JavaScript 





 

@hokaccha

 

JavaScript, Node.js, Ruby
 


1




2



JavaScript  




JavaScriptjQuery

UIUX

CanvasWebGL使
 



JavaScript







 






1. 



prototypeOOP

使API




 

 


2. 



MVC


 

 




1. ModelView



2. View






Model View





Model

View

Controller
 


MVC



MVCMVC



MVVM



MVP



MOVE



MVCn



Backbone.js
ViewController







MV*



MVWTF






使



Model : 

View : DOM

Controller :   



JavaScript


(一)ModelView

(二)View
 


: Todo







$(function() {
  // 要素取得してー
  var $form = $('.todoForm');
  var $input = $form.find('input[type="text"]');
  var $list = $('.todoList');

  // フォームがサブミットされたらー
  $form.submit(function(e) {
    e.preventDefault();

    // 要素作って追加ー
    var text = $input.val();
    var html = '<li><input type="checkbox">' + text + '</li>';
    var $li = $(html);

    $li.find('input[type="checkbox"]').change(function() {
      $(this).closest('li').toggleClass('complete');
    });

    $list.append($li);
  });
});




DOMDOM


 











 



使Todo









$(function() {
  var $form = $('.todoForm');
  var $input = $form.find('input[type="text"]');
  var $list = $('.todoList');
  var $usual = $('.usualList li');

  // 共通の処理(リストを追加する部分)を関数に切り出した
  function addList(text) {
    var html = '<li><input type="checkbox">' + text + '</li>';
    var $li = $(html);

    $li.find('input[type="checkbox"]').change(function() {
      $(this).closest('li').toggleClass('complete');
    });

    $list.append($li);
  }

  // よく使う一覧をクリックしたらリストに追加
  $usual.click(function(e) {
    e.preventDefault();

    var text = $(this).text();
    addList(text);
  });

  // フォームをサブミットしたらリストに追加
  $form.submit(function(e) {
    e.preventDefault();

    var text = $input.val();
    addList(text);
  });
});



















ModelView



Model



model.todo.js

// Todoのデータ管理するModelクラス
function Todo(data) {
  this.text = data.text;
  this.complete = !!data.complete;
}

// 説明簡略化のためBackbone.Eventから拝借したEventをmixin
// onとかtriggerメソッドが使えるようになる
$.extend(Todo.prototype, Events);
$.extend(Todo, Events);

// completeプロパティを変更するメソッド
Todo.prototype.setComplete = function(complete) {
  this.complete = !!complete;
  this.trigger('change:complete', this);
};

// 自身のインスタンスを保持する配列
Todo.list = [];

// 新規Todoを追加するためのクラスメソッド
Todo.add = function(text) {
  var todo = new Todo({ text: text });

  Todo.list.push(todo);
  this.trigger('add', todo);
};




DOM


 


View



View



UI





view.todoForm.js

// Todoを入力するフォームを管理するViewクラス
function TodoFormView($el) {
  this.$el = $el;
  this.$input = this.$el.find('input[type="text"]');
  this.$el.submit(this.onsubmit.bind(this));
}

// サブミット時のイベントハンドラ
TodoFormView.prototype.onsubmit = function(e) {
  e.preventDefault();
  Todo.add(this.$input.val());
};




Model

View
 


view.todoList.js

// Todo一覧のリストを管理するViewクラス
function TodoListView($el) {
  this.$el = $el;
  Todo.on('add', this.add.bind(this));
}

// Todoの要素を追加するメソッド
TodoListView.prototype.add = function(todo) {
  var item = new TodoListItemView(todo);
  this.$el.append(item.$el);
};

// Todo一覧の要素をを管理するViewクラス
function TodoListItemView(todo) {
  this.todo = todo;
  this.$el = $('<li><input type="checkbox">' + todo.text + '</li>');
  this.$checkbox = this.$el.find('input[type="checkbox"]');

  this.$checkbox.change(this.onchangeCheckbox.bind(this));
  this.todo.on('change:complete', this.onchangeComplete.bind(this));
}

// checkboxの値が変わった時のイベントハンドラ
TodoListItemView.prototype.onchangeCheckbox = function() {
  this.todo.setComplete(this.$checkbox.is(':checked'));
};

// モデルのcompleteプロパティの値が変わった時のイベントハンドラ
TodoListItemView.prototype.onchangeComplete = function() {
  if (this.todo.complete) {
    this.$el.addClass('complete');
  }
  else {
    this.$el.removeClass('complete');
  }

  this.$checkbox.attr('checked', this.todo.complete);
};




ModelDOM

View
 


main.js


View
$(function() {
  new TodoFormView( $('.todoForm') );
  new TodoListView( $('.todoList') );
});




ViewModel



View




使Todo



main.js

$('.usualList li').click(function() {
  Todo.add($(this).text());
});

 View






model.todo.js

Todo.setCompleteAll = function() {
  Todo.list.forEach(function(todo) { todo.setComplete(true); });
};

main.js

$('.completeAll').click(function() {
  Todo.setCompleteAll();
});

 View






View



Backbone.js使













HTMLCSS

OK


 




Selenium

capybara

CasperJS
 


CasperJS

var casper = require('casper').create();

casper.start('./index.html', function() {
  this.evaluate(function() {
    var form = document.querySelector('.todoForm');
    form.querySelector('input[type="text"]').value = 'foo';
    form.querySelector('input[type="submit"]').click();
  });
});

casper.then(function() {
  this.test.assertEvalEquals(function() {
    return document.querySelectorAll('.todoList li').length;
  }, 1, 'Added Todo List');

  this.test.assertEvalEquals(function() {
    return document.querySelector('.todoList li').textContent;
  }, 'foo', 'Added input value');
});

casper.run(function() {
  this.test.done();
  this.test.renderResults(true);
});






SeleniumRuby


Firefox
require 'test/unit'
require 'selenium-webdriver'

class TodoAppTest < Test::Unit::TestCase
  def setup
    @driver = Selenium::WebDriver.for :firefox
  end

  def teardown
    @driver.quit
  end

  def test_submit_todo
    url = "file://#{File.expand_path('..', __FILE__)}/todo/index.html"
    @driver.navigate.to url

    input = @driver.find_element :name => 'text'
    input.send_keys 'foo'
    input.submit

    list = @driver.find_elements :css => '.todoList li'
    assert_equal(list.size, 1)
    assert_equal(list[0].text, 'foo')
  end
end








JavaScript
 




Mocha

Jasmine

QUnit

JsTestDriver

Buster.JS
 



Mocha



Mocha








 


Mocha



 
describe('テストの対象', function() {
  it('テストの内容', function() {
    // ここで例外が投げられたらテストが落ちる
  });

  it('テストの内容', function() {
    // ここで例外が投げられたらテストが落ちる
  });
});


describe

describe('テストの対象', function() {
  // describeはネストできる
  describe('テストの対象', function() {
    it('テストの内容', function() {
      // ...
    });
  });

  describe('テストの対象', function() {
    it('テストの内容', function() {
      // ...
    });
  });
});


describe('テストの対象', function() {
  // テストの事前処理
  before(function() {
  });

  // テストの事後処理
  after(function() {
  });

  // itの前に毎回行う処理
  beforeEach(function() {
  });

  // itの後に毎回行う処理
  afterEach(function() {
  });
});


describe('テストの対象', function() {
  // doneを引数に取ると非同期
  it('非同期のテスト', function(done) {
    setTimeout(function() {
      // ここにテストを書く
      done();
    }, 100);
  });
});






Chai

expect.js
 


Chai
expect.js



expect.js



expect(foo).to.be('bar');
expect(foo).to.eql({ foo: 'bar' });
expect(foo).to.have.property('bar', 'baz');
expect(foo).to.be.a(Date);
expect(function() {
  foo();
}).to.throwError();


Mocha + expect.js



describe('Todo', function() {
  describe('.add', function() {
    it('Todo.listにインスタンスが追加されること', function() {
      Todo.add({ text: 'foo' });
      expect(Todo.list).to.have.length(1);
      expect(Todo.list[0]).to.be.a(Todo);
    });
  });

  describe('#setComplete', function() {
    var todo;
    beforeEach(function() {
      todo = new Todo({});
    });

    it('completeが設定されること', function() {
      todo.setComplete(true);
      expect(todo.complete).to.be(true);
    });

    it('change:completeイベントが発火すること', function(done) {
      todo.on('change:complete', function() {
        expect(todo.complete).to.be(true);
        done();
      });
      todo.setComplete(true);
    });
  });
});


Sinon.js







 (Test Double) 
  - Wikipedia  


Sinon.js



spy

stub

mock

Fake Timer

Fake XHR
 


spy



調

// Todo.addにspyを忍ばせる
var spy = sinon.spy(Todo, 'add');

// Todo.addメソッドを実行
Todo.add('foo');

// メソッドが呼ばれたかどうかや引数を調べられる
expect(spy.calledOnce).to.ok();
expect(spy.args[0][0]).to.be('foo');

// spyを解除
spy.restore();


stub



window.confirm

ListView.prototype.remove = function() {
  if (window.confirm('削除しますか?')) {
    this.$el.remove();
    this.$el = null;
  }
};





confirm

describe('ListView', function() {
  var listView;
  beforeEach(function() {
    listView = new ListView($('<ul>'));
  });

  describe('#remove', function() {
    it('要素が削除されること', function() {
      listView.remove();
      expect(listView.$el).to.be(null);
    });
  });
});




stub使



describe('ListView', function() {
  describe('#remove', function() {
    var stub;
    var listView;
    beforeEach(function() {
      stub = sinon.stub(window, 'confirm');
      listView = new ListView($('<ul>'));
    });
    afterEach(function() {
      stub.restore();
    });

    it('window.confirmが呼ばれること', function() {
      listView.remove();
      expect(stub.calledOnce).to.ok();
      expect(stub.args[0][0]).to.be('削除しますか?');
    });

    context('window.confirmでOKを押したとき', function() {
      beforeEach(function() {
        stub.returns(true);
      });

      it('要素が削除されること', function() {
        listView.remove();
        expect(listView.$el).to.be(null);
      });
    });

    context('window.confirmでキャンセルを押した時', function() {
      beforeEach(function() {
        stub.returns(false);
      });

      it('要素が削除されないこと', function() {
        listView.remove();
        expect(listView.$el).to.not.be(null);
      });
    });
  });
});


Sinon.js
便





Q. Model
View


A. 



TodoFormView

describe('TodoFormView', function() {
  var todoForm;
  var html = '<form><input type="text"></form>';

  beforeEach(function() {
    todoForm = new TodoFormView($(html));
  });

  it('$elに要素がセットされていること', function() {
    expect(todoForm.$el.is('form')).to.ok();
  });

  context('submitしたとき', function() {
    var spy;
    beforeEach(function() {
      spy = sinon.spy(Todo, 'add');
      todoForm.$input.val('foo');
      todoForm.$el.submit();
    });
    afterEach(function() {
      spy.restore();
    });

    it('textの値がTodo.addに渡されること', function() {
      expect(spy.calledOnce).to.ok();
      expect(spy.args[0][0]).to.be('foo');
    });
  });
});


View



Q. UI



A. 








 



flipsnap.js





1

2

3





dispatchEvent
// jQueryのtriggerのようなもの
function trigger(element, eventType, params) {
  var ev = document.createEvent('Event');
  ev.initEvent(eventType, true, false);
  $.extend(ev, params || {});
  element.dispatchEvent(ev);
}

// タッチイベントを発火させて現在地が動くかをテストする
it('should move to next', function() {
  trigger(f.element, touchStartEvent, { pageX: 50, pageY: 0 });
  expect(f.currentPoint).to.be(0);

  trigger(f.element, touchMoveEvent, { pageX: 40, pageY: 0 });
  trigger(f.element, touchMoveEvent, { pageX: 30, pageY: 0 });
  expect(f.currentPoint).to.be(0);

  trigger(document, touchEndEvent);
  expect(f.currentPoint).to.be(1);
});





Q. CI



A. 



DEMO
PhantomJS + Mocha



DEMO
testem













There's More Than
One Way To Do It.



Enjoy Programming !!