読者です 読者をやめる 読者になる 読者になる

AngularJSで$templateCacheを使っている場合のテスト環境

JavaScript

ちなみにgenerator-angularで作成したプロジェクト。

developmentやbuildではgrunt-angular-templatesを使っているが、テストには、karma-ng-html2js-preprocessorを使った。

インストール

% npm install karma-ng-html2js-preprocessor --save-dev

karma.confに設定を追加

#karma.conf.coffee

    files: [
      # ...省略...
      "app/templates/*.html"  #<- テンプレートファイルを追加
      "test/mock/**/*.coffee"
      "test/spec/**/*.coffee"
    ]

    plugins: [
      "karma-phantomjs-launcher"
      "karma-jasmine"
      "karma-coffee-preprocessor"
      "karma-spec-reporter"
      "karma-coverage"
      "karma-ng-html2js-preprocessor"  #<- 追加
    ]

    preprocessors:
      "app/templates/*.html": ["ng-html2js"] # <- 追加
      "app/scripts/*.coffee": "coffee"
      "app/scripts/*/**/*.coffee": "coverage"
      "test/**/*.coffee": "coffee"

  # 追加
    ngHtml2JsPreprocessor:
      stripPrefix: "app/"
      moduleName: "test.templates" #<- 本番とは別に任意に設定した

あとはtemplateCacheのテンプレートを使っている部分のテストで beforeEach module "myApp", "test.templates"とか読みこめばOK.

参考

AngularJSのカステムディレクティブで別のディレクティブをrequireしている場合のテストの書き方

JavaScript

前の記事のでいえばchildItemディレクティブのテストの書き方

コード再掲

#parentChild.coffee


"use strict"

#parent
#controller

angular.module("myApp")
.controller("ParentItemCtrl", ($scope, Child) ->
  #.......
  @removeChildItem = (childId, index)->
    Child.destroy({childId: childId}, ->
      $scope.parent.cildren.splice(index, 1)
    )
)

#parent directive

angular.module("myApp")
.directive("parentItem", ->
  restrict: "EA"
  templateUrl: "templates/parent_item.html"
  controller: "ParentItemCtrl"
  scope:
    parent: "=item"
)

#child directive

angular.module("myApp")
.directive("childItem", ($modal)->
  restrict: "EA"
  templateUrl: "templates/child_item.html"
  require: "^parentItem"
  scope:
    child: "=item"
  link: (scope, element, attrs, parentItemCtrl) ->
    scope.open = ()->
      modalInstance = $modal.open({
        templateUrl: "/remplates/modal_confirm.html"
      })

      modalInstance.result.then(->
        parentItemCtrl.removeChildItem(scope.child.id, scope.$index) 
      )
)

こんな感じでテストを書いた

#child_item_spec.coffee
"use strict"

describe "Directive: childItem", ->

  beforeEach module "myApp", "test.templates"

  scope = {}

  beforeEach inject ($controller, $rootScope) ->
    scope = $rootScope.$new()

  it "テンプレートに所定のデータが表示されること", inject ($compile) ->
    scope.child = {
      "id": 406
      "content": "child4"
      "created_at": "2014-09-23T11:16:23.609+09:00"
      "user":
        "id": 5018
        "profile":
          "name": "あい うえお"
    }
    element = angular.element '''<child-item item="child"></child-item>'''
    parentItemController = {
      removeChildItem: -> return
    }
    spyOn(parentItemController, "removeChildItem").andCallThrough()
    element.data("$parentItemController", parentItemController)
    element = $compile(element)(scope)
    childItemScope = element.find("child-item").scope()
    scope.$digest()
    expect(element.find(".child-user-name").text()).toEqual "あい うえお"

ChromeのDeveloper Toolsで確認するとわかるが、'$' + ディレクティブの名前 + 'Controller'という名前でcontrollerを取得しているので、それにあわせてmockを用意してあげればOK.
ちなみにディレクティブのhtmlは$templateCacheを使っていて、beforeEach module "myApp", "test.templates"の部分で、テスト用にテンプレートをJSにまとめたものを読み込んでいる。こちらについては別のエントリに書く。→書いた

参考

AngularJSを1.2系から1.3.0にあげたらrequireした親のコントローラを使っているdirectiveでエラーが出た

JavaScript

コードはこんな

"use strict"

#parent
#controller

angular.module("myApp")
.controller("ParentItemCtrl", ($scope,Child) ->
  #.......  coffeeなので自動で波括弧をつけてくれるが明示的につけておいた ............
  {
    removeChildItem: (childId, index)->
      Child.destroy({childId: childId}, ->
        $scope.parent.children.splice(index, 1)
      )
  }
)

#directive

angular.module("myApp")
.directive("parentItem", ->
  restrict: "EA"
  templateUrl: "templates/parent_item.html"
  controller: "ParentItemCtrl"
  scope:
    parent: "=item"
)

#child directive

angular.module("myApp")
.directive("childItem", ($modal)->
  restrict: "EA"
  templateUrl: "templates/child_item.html"
  require: "^parentItem"
  scope:
    child: "=item"
  link: (scope, element, attrs, parentItemCtrl) ->
    scope.open = ()->
      modalInstance = $modal.open({
        templateUrl: "/remplates/modal_confirm.html"
      })

      modalInstance.result.then(->
        parentItemCtrl.removeChildItem(scope.child.id, scope.$index) 
     # ↑ ここでremoveChildItemがないとエラー
      )
)

controllerの関数で公開したいものだけ(この場合はremoveChildItem)をモジュールパターンを使って返していて、1.2系では動いていたけど、1.3では動かなくなった。 chromeのDeveloper Toolsで確認すると、parentItemCtrlは、1.2系では期待通りオブジェクトがわたってきていたが、1.3では$get.Constructorとなっていた。

↓1.2 1.2.png

↓1.3 1.3.png

ので、コントローラを以下のように変更した。

# parent_item.coffee

angular.module("myApp")
.controller("ParentItemCtrl", ($scope, Child) ->

  @removeChildItem = (childId, index)->
    Child.destroy({childId: childId}, ->
      $scope.parent.cildren.splice(index, 1)
    )
)

RailsでPunditとshoulda-matchersを併用して、Rspecのcustom matchersが被った

Rails

Punditとshoulda-matchersにはpermitという同名のカスタムマッチャーがあって、衝突してしまう。

ので、punditのpermitは、spec_helper.rbでのrequire "pundit/rspec"をやめて、punditのカスタムマッチャーのpermitを使いたい個別のspecファイル、例えばuser_policy_spec.rbで、require "pundit/rspec"して解決した。

AngularJSでネストしたコントローラをテストする

JavaScript

前提

AngularJSでコントローラをネストして階層化すると、子コントローラ側から親コントローラのスコープを利用可能になる。

問題

子コントローラのユニットテストを書こうとした場合に、子コントローラ側で親コントローラのプロパティやメソッドを使用していると、そのままではテストが書けない。

解決

以下のように記述した。
ReportShowCtrlが親コントローラ、CommentNewCtrlが子コントローラ。

# comment_new_spec.coffee

"use strict"

describe "Controller: CommentNewCtrl", ->

  beforeEach module "myApp"

  CommentNewCtrl = {}
  scope = {}
  Comment = {}

  beforeEach inject ($controller, $rootScope) ->
    $controller("ReportShowCtrl", {
      $scope: $rootScope
      report: {id: 1}
    })
    scope = $rootScope.$new()
    CommentNewCtrl = $controller "CommentNewCtrl", {
      $scope: scope
    }

  it "some test", ->
    # do something

そもそも論としてネストしたコントローラで、親のプロパティやメソッドが〜、というのは可読性やテスタビリティが下がるのでアレではある。

派生したスコープでのハマリもあるし。

AngularJSのフォームで投稿後に再利用する際手付かずの状態に戻したい

JavaScript

例えば記事へのコメント投稿後に、そのフォームを再利用する際に、データを初期化しただけだとエラー表示が出てしまう。

ので、$dirtyがfalse($pristineがtrue)の状態に戻したい。

いわゆるブログのコメント投稿とかのよくあるやつ。

form.FormControllerの$setPristine()を使う。

# comment_new.html
  <div ng-controller="CommentNewCtrl">
    <form name="commentNewForm">
      <div class="form-group"
           ng-class="{'has-error': commentNewForm.content.$dirty && commentNewForm.content.$invalid }">
        <textarea class="form-control" ng-model="comment.content" placeholder="" tabindex="1" name="content"
                  required></textarea>
        <span ng-show="commentNewForm.content.$dirty&&commentNewForm.content.$error.required">
          必須です
        </span>
      </div>
      <button ng-disabled="!commentNewForm.$valid" ng-click="save()">コメントを投稿</button>
    </form>
  </div>
# comment_new.coffee
angular.module('myApp')
.controller 'CommentNewCtrl', ($scope, Comment) ->

# .........................

  $scope.save = ->
    $scope.comment.$create((data)->
      # .........................
      $scope.comment = new Comment(report_id: reportId) #<- 初期化
      $scope.commentNewForm.$setPristine()  # <- コレ
    )

OK.

RailsのResourcesでNestしてshallow: trueしたresourcesをAngularJSの$resourceで扱う

JavaScript Rails

例えばこういう感じのresourcesを定義したとする。

APIの例。newとeditは省いた。

# routes.rb
namespace :api, defaults: { format: :json } do
  resources :reports, shallow: true do
    resources :comments, except: [:new, :edit]
  end
end

定義されるルーティングはこうなる。reportsのは話に関係ないので省略

GET  /api/reports/:report_id/comments(.:format)  api/comments#index {:format=>:json}

POST /api/reports/:report_id/comments(.:format) api/comments#create {:format=>:json}

GET /api/comments/:id(.:format) api/comments#show {:format=>:json}

PATCH   /api/comments/:id(.:format) api/comments#update {:format=>:json}

PUT /api/comments/:id(.:format) api/comments#update {:format=>:json}

DELETE  /api/comments/:id(.:format) api/comments#destroy {:format=>:json}

Railsではよくあるパターンだと思う。

このコメントのresourceをAngularJSの$resourceで使いたい。

$resourceでよく見かけるサンプルだとシンプルなものが多い.

こういったことは出来ないんだろうかと、AngularJSのリファレンスで$resourceの項目を見てみたら、ちゃんとやり方が書いてあった。

url – {string} – action specific url override. The url templating is supported just like for the resource-level urls.

$resourceの第3引数のactionsでurlをoverrideすればよいと。

やってみた。

下記はqueryとかsaveとか自動のやつは無視してRails的なactionを定義

# Comment.coffee
'use strict'

angular.module('myApp')
.factory 'Comment', ($resource) ->
  $resource("/api/comments/:commentId", {commentId: "@id", reportId: "@report_id"}, {
    index: {method: "GET", url: "/api/reports/:reportId/comments", isArray: true}
    create: {method: "POST", url: "/api/reports/:reportId/comments"}
    update: {method: "PATCH",params: {commentId: "@id"}}
    show: { method: "GET",params: {commentId: "@id"}}
    destroy: {method: "DELETE",params: {commentId: "@id"}}
  })

使う例

# sample.coffee
angular.module('myApp')
.controller "SampleCtrl", ($scope, $stateParams, Comment) ->
  $scope.comments = Comment.index(reportId: $stateParams.reportId)

  $scope.comment = new Comment(report_id: $stateParams.reportId)

  $scope.save = ->
    $scope.comment.$create((comment)->
      # do something
 

上記の場合だと、report_idとreportIdでもにょってしまうので、サービス側でもう少しラップしてオブジェクトの引数ではなく、パラメータで渡した方が幸せになれるかもしれない。