AngularJSで$templateCacheを使っている場合のテスト環境
ちなみに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している場合のテストの書き方
前の記事のでいえば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でエラーが出た
コードはこんな
"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.3
ので、コントローラを以下のように変更した。
# parent_item.coffee angular.module("myApp") .controller("ParentItemCtrl", ($scope, Child) -> @removeChildItem = (childId, index)-> Child.destroy({childId: childId}, -> $scope.parent.cildren.splice(index, 1) ) )
AngularJSでネストしたコントローラをテストする
前提
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のフォームで投稿後に再利用する際手付かずの状態に戻したい
例えば記事へのコメント投稿後に、そのフォームを再利用する際に、データを初期化しただけだとエラー表示が出てしまう。
ので、$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で扱う
例えばこういう感じの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でもにょってしまうので、サービス側でもう少しラップしてオブジェクトの引数ではなく、パラメータで渡した方が幸せになれるかもしれない。