仙台 Ruby Vim JavaScript 社長

片平堂のブログ

Yeomanのgenerator-angularで作ったプロジェクトにE2EテストフレームワークのProtractorを導入してCoffeeScriptで書いたテストを実行する

generator-angularはkarma,jasmineのtestは用意されているけど、e2eテストはAngular Scenario Runner(非推奨)が入ってはいるものの使えるようになってないので、Protractorを入れる。

前提

yo angular --coffee でフロントエンド部を生成済みとする*1

作業

% cd ngapp
% bower uninstall -D angular-scenario
% npm install -D protractor
% npm install -D grunt-protractor-runner
% npm install -D protractor-coffee-preprocessor
% npm install -D grunt-exec
% mkdir test/e2e

test/下にprotractorの設定ファイルを置いてみた

// test/protractor.conf.js

"use strict";

exports.config = {
  // Seleniumサーバーのアドレス
  seleniumAddress: "http://localhost:4444/wd/hub",
  // テストで利用するブラウザの条件を設定
  // 詳細は https://code.google.com/p/selenium/wiki/DesiredCapabilities
  capabilities: {
    browserName: "chrome"
  },
  // テスト対象のspecファイルのパス(この設定ファイルからの相対パス)
  specs: [
    "e2e/**/*.coffee"
  ],
  // テスト対象のアプリケーションのベースURL
  baseUrl: "http://localhost:9001",
  framework: "jasmine",
  plugins: [
    "protractor-coffee-preprocessor"
  ],
  // Disable animations so e2e tests run more quickly
  onPrepare: function () {
    // Disable animations so e2e tests run more quickly
    var disableNgAnimate = function () {
      angular.module("disableNgAnimate", []).run(["$animate", function ($animate) {
        $animate.enabled(false);
      }]);
    };

    browser.addMockModule("disableNgAnimate", disableNgAnimate);

    // Store the name of the browser that's currently being used.
    browser.getCapabilities().then(function (caps) {
      browser.params.browser = caps.get("browserName");
    });
  },
  jasmineNodeOpts: {
    showColors: true,
    isVerbose: false,
    defaultTimeoutInterval: 30000
  }
};

Gruntfile.coffeeに追加。

# Gruntfile.coffee
module.exports = (grunt) ->
  grunt.initConfig
# ................................省略.................................

  # E2E test
    protractor:
      options:
        keepAlive: true
        noColor: false
      coffee:
        configFile: "test/protractor.conf.js"

    exec:
      webdriverUpdate: "node_modules/protractor/bin/webdriver-manager update"
# ................................省略.................................
   grunt.registerTask "test", [
    "exec:webdriverUpdate"    # <- 追加
     "clean:server"
     "concurrent:test"
     "autoprefixer"
     "connect:test"
     "karma"
    "protractor"  # <- 追加
   ]

上記は手抜きでtestのタスクに混ぜてしまっているが、ちゃんとunitテストのとは別にtest:protractorとかにしといた方がいいと思う。

正しく設定出来てテストが動くか確認のために、サンプルを用意。 こちらのを使わせていただきました。HTMLにidが入っていなかったのでそこだけ追加。

<!-- main.html-->
<div class="jumbotron">
  <h1>'Allo, 'Allo!</h1>
  <p class="lead">
    <img src="images/yeoman.png" alt="I'm Yeoman"><br>
    Always a pleasure scaffolding your apps.
  </p>
  <p><a class="btn btn-lg btn-success" ng-href="#">Splendid!<span class="glyphicon glyphicon-ok"></span></a></p>
  <p><a class="btn btn-lg btn-success" ng-click="click()" id="ShowListBtn">Add List<span class="glyphicon glyphicon-ok"></span></a></p>
</div>
<div class="row marketing" ng-if="visibleList">
  <ul id="awesomeThings" ng-repeat="awesomeThing in awesomeThings">
    <li>{{awesomeThing}}</li>
  </ul>
</div>

テスト記述

# test/e2e/main_spec.coffee
'use strict'

describe 'E2ETestSample',->
  beforeEach ->
    browser.get('/#/')

  it 'ボタン押下後、3つのリストが出る事の確認',->
    expect(element.all(By.repeater('awesomeThing in awesomeThings')).count()).toEqual(0)
    button = browser.findElement(By.css('#ShowListBtn'))
    button.click()
    expect(element.all(By.repeater('awesomeThing in awesomeThings')).count()).toEqual(3)

テスト実行

% node_modules/protractor/bin/webdriver-manager update
% node_modules/protractor/bin/webdriver-manager start
% grunt test

OK.

f:id:yuichi_katahira:20140818120356p:plain

上記ではseleniumのスタートを手動で叩いているけど、seleniumをStandalone Server as a Serviceとするのが良さ気↓

追記

grunt testを、unit,e2e,全部実行としてみた。

# Gruntfile.coffee

  grunt.registerTask "test", (target) ->
    if target is "unit"
      grunt.task.run([
        "clean:server"
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "karma"
      ])
    else if target is "e2e"
      grunt.task.run([
        "exec:webdriverUpdate"
        "clean:server"
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "protractor"
      ])
    else
      grunt.task.run([
        "exec:webdriverUpdate"
        "clean:server"
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "karma"
        "protractor"
      ])

これでgrunt test:unitでkarma, grunt test:e2eでprotractor, grunt testで両方実行出来る。

参考

*1:generator-angular

Yeomanのgenerator-angularとRailsの組み合わせでの開発環境構築

grunt-connect-proxyを使って、rails server と、grunt serve の2つを叩いてLiveReloadで開発出来るようにしようという話。
あとフロント側をビルドするとRailsのpublicディレクトリに静的ファイルとして配備するように。

RailsのAsset PipelineにAngularJSを載せるんじゃなくて、サーバーサイド(Rails)とクライアントサイド(AngularJS)を分離しての開発の話。

こういう事↓
理想的な Rails, AngularJS 環境の構築 - ボクココ

前提

Railsアプリ作成して、apiのnamespaceでAPIを作成済み、Railsアプリのルートにngappとかディレクトリ作って、yo angular --coffee でフロントエンド部を生成済みとする。*1

作業

% cd ngapp
% npm install -D grunt-connect-proxy
# Gruntfile.coffee

"use strict"

proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest
# ↑追加

module.exports = (grunt) ->

  require("load-grunt-tasks")(grunt)

  require("time-grunt")(grunt)

  # Configurable paths for the application
  appConfig =
    app: require("./bower.json").appPath or "app"
    dist: "../public"  # ←修正. railsのpublicをdistに

  # Define the configuration for all the tasks
  grunt.initConfig

  # Project settings
    yeoman: appConfig

# ................................省略.................................

    connect:
      options:
        port: 9000

      # Change this to '0.0.0.0' to access the server from outside.
        hostname: "localhost"
        livereload: 35729

      livereload:
        options:
          open: true
          middleware: (connect) ->
            [
              proxySnippet  #<- 追加
              connect.static(".tmp")
              connect().use("/bower_components", connect.static("./bower_components"))
              connect.static(appConfig.app)
            ]
      proxies: [      #<- proxies: [...] 追加
        context: '/api'
        host: 'localhost'
        port: '3000'
      ]
 
# ................................省略.................................     

  grunt.registerTask "serve", "Compile then start a connect web server", (target) ->
    if target is "dist"
      return grunt.task.run([
        "build"
        "connect:dist:keepalive"
      ])
    grunt.task.run [
      "clean:server"
      "wiredep"
      "concurrent:server"
      "configureProxies"  #<- 追加
      "autoprefixer"
      "connect:livereload"
      "watch"
    ]

# ................................省略.................................   
% bundle exec rails server
% grunt serve

これで、localhost:9000/にアクセスするとフロント側、localhost:9000/apiRailsAPI側に*2
APIとは別に、管理系とかを普通にRailsアプリとして作って同じプロジェクト上にある場合は、Railsのお作法に則り、localhost:3000/下にアクセスでいいと思う。

ビルドは,--forceを忘れずに

grunt build --force

参考

*1:generator-angular

*2:localhost:9000/apiRailsのrootという意味ではありません

Deviseでnamespace使う場合はdevise_for :users, path: :adminとかしとけという話

メモ。

Devise使用時にroutesでadminとかのnamespaceにdevise_forを入れると,Devise::SessionsController等で authenticate_user! が authenticate_admin_user! とかなってしまう。

# config/routes.rb
namespace :admin do
  devise_for :users, controllers: {
    sessions: "admin/users/sessions",
    ......
  }
end

devise_for with namespace generate wrong methods · Issue #412 · plataformatec/devise · GitHub

こう書く。

# config/routes.rb
 devise_for :users, path: :admin, controllers: {
    sessions: "admin/users/sessions",
    ......
}

今回は諸事情があってDevise使ってるけど、認証はRails提供のsecure_passwordとか使って、自前で書いたほうがはまらなくていいと思う。大分楽に書けるようになったし。Deviseはカスタマイズしようとすると面倒くさい。

Middlemanの入れ子レイアウトでエラー undefined method `safe_concat' for "":String

slimでwrap_layout使おうとしてエラーった。- を = に。

wrap_layout doesn't work with slim

参考: wrap_layout doesn't work with slim on ~> 3.3.2 #1269

gulpfileをCoffeeScriptで書く

メモ。

//gulpfile.js
require('coffee-script/register');
require('./gulpfile.coffee');
//gulpfile.coffee
gulp = require('gulp')

gulp.task 'default', () ->
  console.log('gulp!')

参考。

普通にCoffeeScriptで書けるようになってた。

node-webkitでコールバックで書いた非同期処理をPromise使用に書き換えてみた

node-webkitで作ったアプリで、データロード処理と保存処理部分をそれぞれ書き換えてみた。
PromiseのライブラリはBluebirdを使ってみた。
あと、cheerioをDOMのパースに使用。
FTPは、jsftpを使用。

データロード処理の流れ(load)
1. FTPでデータ取得
2. FTPで取得したファイルを読み込み
3. 読み込んだHTMLをパースしてデータ取得
4. 入力フォームに取得したデータを反映

保存処理の流れ(save)
1. FTPで取得していたファイルを読み込み
2. 読み込んだHTMLをパースしてフォームに入力されたデータをHTMLに反映
3. フォームのデータが反映されたHTMLのデータを一時ファイルに書き込み
4. 書き込まれた一時ファイルをFTPでアップロード
5. アップロード完了後、別画面に遷移、完了通知などの処理

v1がコールバック版、v2がPromise版

callback版、Promise版

2014/6/25追記:loadとsaveを呼び出している部分(index.coffee)も追加してみた。

File SystemのAPIはBluebirdのPromisificationという機能を使ってみた。

HTML部分の入力フォームやFTPの設定などはgistに上げてませんが、大体やってる流れは把握できるかと。

cheerioでパースすると、日本語が数値文字列参照になっているので、変換。
https://github.com/cheeriojs/cheerio/issues/466

Promiseについては以下が大変参考になりました。