Vagrant環境でProtractorで使うSeleniumをstandalone serverにした

generator-angularで作ったプロジェクトにProtractorでのe2eテストを入れたやつをVagrant環境でアレする。

コレの続き

https://github.com/exratione/protractor-selenium-server-cookbook:このクックブックを追加してよしなに設定する。

で、node_modules/protractor/bin/webdriver-manager updateいらないと思うので消す。

# 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", (target) ->
    if target is "unit"
      grunt.task.run([
        "clean:server"
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "karma"
      ])
    else if target is "e2e"
      grunt.task.run([
        "clean:server"  #<- "exec:webdriverUpdate"の記述を削除した
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "protractor"
      ])
    else
      grunt.task.run([
        "clean:server"
        "concurrent:test"
        "autoprefixer"
        "connect:test"
        "karma"
        "protractor"
      ])

参考

Yeomanのgenerator-angularで作ったプロジェクトをVagrant環境で開発する場合の設定

Yeomanで作ったプロジェクト関係なくて、単にVagrant環境で開発する時にホスト側のブラウザとかで確認したい時の話。

こういう場合にgrunt-contrib-connectの設定で、こういうのを見かける↓

#Gruntfile.coffee
     connect: 
       options: 
         port: 9000
         livereload: 35729
         # change this to '0.0.0.0' to access the server from outside
         # hostname: 'localhost'
         hostname: '192.168.x.x'

これだと、ゲスト側のIPアドレスを覚えていないといけない。

ので、こうした。

#Vagrantfile

Vagrant.configure("2") do |config|
  config.vm.hostname = "hostname"
  config.vm.box = "some/box"
  config.vm.network :private_network, ip: "192.168.x.x"
  config.vm.network "forwarded_port", guest: 3000, host: 3000
  config.vm.network "forwarded_port", guest: 9000, host: 9000
  config.vm.network "forwarded_port", guest: 35729, host: 35729
  # ........
#Gruntfile.coffee
   connect:
      options:
        port: 9000
      # Change this to '0.0.0.0' to access the server from outside.
        hostname: "0.0.0.0"
        livereload: 35729

これでホスト側でlocalhost:9000にアクセスすればOK。
192.168.x.x:9000でもアクセス出来る。
ライブリロードも効く。NFS使ってるとライブリロードがちょっと遅いのが難点。そこだけどうにかしたい。

後、Gruntfileでhostをlocalhostと設定していたところは127.0.0.1に修正しといた。 ちなみにUbuntuのイメージ使ってる。

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

2014/8/23追記

Vagrantの場合↓ Yeomanのgenerator-angularで作ったプロジェクトをVagrant環境で開発する場合の設定 - 仙台 Ruby Vim JavaScript 社長

参考

*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! とかなってしまう*1

# 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はカスタマイズしようとすると面倒くさい。

*1:注:カスタムしたコントローラーに任意のアクションを突っ込んでしまっているからです。