AngularJSコンポーネントのユニットテスト
よく忘れるので整理。
以下のようなFooComponentをテストする場合。
/** * 初期化されたらステータスをactiveにし、タイトルを描画する * タイトル下部に外部から渡されたテキストを描画する */ class FooController { private active: boolean = false; /*@ngInject*/ constructor(private $element) { } $onInit() { this.active = true; this.render(); } render() { const elm = this.$element.find('h1'); elm.text('foo'); } doSomething() { return 'Do something'; } } const FooComponent: ng.IComponentOptions = { template: '<h1></h1><div>{{ $ctrl.bar }}</div>', controller: FooController, bindings: { bar: '<' } }; export default FooComponent;
$componentControllerを利用
describe('FooComponent', () => { let controller; beforeEach(angular.mock.module('app')); beforeEach(inject(($componentController, $rootScope) => { const locals = { $scope: $rootScope.$new(), /** * Componentが$elementを利用している場合、localsに追加する必要がある * * https://docs.angularjs.org/api/ngMock/service/$componentController * > If you are using $element or $attrs in the controller, * > make sure to provide them as locals. */ $element: angular.element('<div></div>') }; const bindings = { bar: 'bar' }; controller = $componentController('foo', locals, bindings); })); it('should be active', () => { expect(controller.active).toBeFalsy(); // $onInitは直接実行 controller.$onInit(); expect(controller.active).toBeTruthy(); }); it('doSomethingを実行', () => { expect(controller.doSomething).toBeDefined(); expect(controller.doSomething()).toBe('Do something'); }); });
コントローラーのテストのみ可能。
https://docs.angularjs.org/guide/component
$compileを利用
describe('FooComponent', () => { let element; let parentScope; beforeEach(angular.mock.module('app')); beforeEach(inject(($compile, $rootScope) => { parentScope = $rootScope.$new(); // element = angular.element('<foo bar="bar"></foo>'); // element = $compile(element)(parentScope); // こっちでも問題ない? element = $compile('<foo bar="bar"></foo>')(parentScope); parentScope.bar = 'bar'; parentScope.$digest(); })); it('bindingsされたbarがhtmlに反映される', () => { expect(element.find('div').text()).toBe('bar'); }); it('$onInit -> render -> タイトルが変更される', () => { // 実際のComponent lifecycleと同じで、$onInitは自動実行される expect(element.find('h1').text()).toBe('foo'); }); it('doSomethingを実行', () => { // controllerを取得 const controller = element.controller('foo'); expect(controller.doSomething).toBeDefined(); expect(controller.doSomething()).toBe('Do something'); }); });
実際の挙動に近い形でコンポーネントのテストが可能。
https://docs.angularjs.org/guide/unit-testing
参考
- https://velesin.io/2016/08/23/unit-testing-angular-1-5-components/
- $componentController, $compileどちらを使うべきか、の話
- $compileを利用したコンポーネントテストのサンプルがまとまっている
- https://github.com/Puigcerber/angular-unit-testing
- Directives以外も含め、ユニットテストのコードがまとまっている
bundlerとCocoaPodsすら知らない
- bundler
- gemを管理するツール
- 自身もgem
- Gemfileを使う
- Gemfileはpackage.jsonのようなもの
- CocoaPods
このようなプロジェクトファイルが既にある場合、
├── Gemfile ├── Gemfile.lock ├── Podfile ├── Podfile.lock ├── README.md ├── foo ├── foo.xcodeproj └── foo.xcworkspace
以下のように始める。
# bundlerをインストール gem install bundler # bundlerを使い、gemをインストール bundle install --path vendor/bundle # CocoaPodsを使い、iOSのパッケージをインストール bundle exec pod install
Swiftの勉強
はじめるわ
HTML Element の幅と高さ、座標。
幅と高さ
Determining the dimensions of elements | MDN
- HTMLElement.offsetWidth
- content + padding + boarder + scrollbar
- Element.clientWidth
- content + padding
- Element.scrollWidth
- element のコンテンツの幅か、element 自身の幅(clientWidth)のうち大きい方を返す
補足
https://www.w3.org/TR/CSS22/box.html#box-dimensions
座標
-
offsetParent
からの距離
Element.getBoundingClientRect()
- viewport (window) からの座標を返す
-
- element のコンテンツがどのくらいスクロールされたか
補足
offsetParent
The offsetParent property returns the nearest ancestor that has a position other than static. http://www.w3schools.com/jsref/prop_element_offsetparent.asp
ドキュメントからの座標
// https://github.com/oneuijs/You-Dont-Need-jQuery function getOffset (el) { const box = el.getBoundingClientRect(); return { top: box.top + window.pageYOffset - document.documentElement.clientTop, left: box.left + window.pageXOffset - document.documentElement.clientLeft }; }
参考
Promiseの配列を順に処理
reduce
を使い新しいpromiseを作成していく。
const tasks = [ () => $q(resolve => doneAsync(resolve)), () => $q(resolve => doneAsync(resolve)) ]; function doneAsync(resolve) { setTimeout(() => resolve(), 1000); } tasks.reduce((promise, task) => { return promise.then(task); }, $q.resolve()) // $q.resolve()で最初のPromiseを生成 .then(() => { console.log('done'); });
実行結果を保存するrecord
処理を追加。
const tasks = [ (value) => $q(resolve => doneAsync(resolve, value)), (value) => $q(resolve => doneAsync(resolve, value)) ]; const record = (results, value) => { results.push(value); return results; }; // [] に実行結果を保存する const _record = record.bind(null, []); tasks.reduce((promise, task) => { return promise.then(task).then(_record) }, $q.resolve(0)) .then(value => { console.log(value); // -> [1, 2] }); function doneAsync(resolve, value) { setTimeout(() => resolve(++value), 1000); }
背景(領域外)をクリックしたら閉じる、とか
ドロップダウンなどを表示したときに、領域外をクリックしたら閉じたい。 HTMLだと、どのように実装するのが一般的なのかわからなかったのでAngularのUI Bootstrapを参考にしてみたよ。
Dropdown
documentにイベントハンドラを登録。
// https://github.com/angular-ui/bootstrap/blob/v1.1.0/src/dropdown/dropdown.js#L13 $document.on('click', closeDropdown); // ... var closeDropdown = function(evt) { // ... // dropdown内をクリックかつ'outsideClick'が設定されている場合は閉じない var dropdownElement = openScope.getDropdownElement(); if (evt && openScope.getAutoClose() === 'outsideClick' && dropdownElement && dropdownElement[0].contains(evt.target)) { return; } // ... openScope.isOpen = false; // ... };
補足: dropdownは複数開くことができないようになっている。uibDropdownServiceが管理している。
Modal
自身(全領域カバーしている)にイベントハンドラを登録。
// https://github.com/angular-ui/bootstrap/blob/v1.1.0/src/modal/modal.js#L165 element.on('click', scope.close);
documentに登録する方法だとこんな感じでしょうか。
class Controller { closeHandler: () => void; /*@ngInject*/ constructor( private $document: ng.IDocumentService, private $element: ng.IAugmentedJQuery, private $scope: ng.IScope ) { } $onInit() { this.closeHandler = () => { this.close(); this.$scope.$apply(); }; this.$element.on('click', ($event) => { // this.$document.on を無効にするため $event.stopPropagation(); }); } $onDestroy() { this.close(); } open() { // 何かの処理 // ... this.$document.on('click', this.closeHandler); } close() { // 何かの処理 // ... this.$document.off('click', this.closeHandler); } } const components: ng.IComponentOptions = { templateUrl: 'foo.html', controller: Controller, bindings: {} }; export default components;
Angularを1.5から1.6に移行
1.5.6から1.6.1に移行。参考にしたのは
- Angular 1.6 is here, this is what you need to know
- https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
など。
対応箇所
$onInit
初期化のロジックが変更。(bindings
された変数がconstructor
内では利用できなくなった。)
全てのDirectiveとComponentに$onInit
を用意し、初期化時の処理を$onInit
内に記述。
(この変更は、書き方が統一されるので嬉しい。)
The reasoning behind this change is that it is not idiomatic JavaScript to bind properties to an Object before its constructor has been called ...
$http
$httpのsuccess()
とerror()
がなくなった。then()
とcatch()
に置き換え。
hash-prefix
hash-prefixが!
に変更。
$q
PromiseのRejectionが未処理の場合、警告が出るようになった。
Possibly unhandled rejection:
$uibModalの利用している場合、backdropクリックでモーダルを閉じると警告がでるので、一応以下のように対応。
this.$uibModal .open({ templateUrl: 'demo.html', }) .result.catch(angular.noop);
メモ
errorOnUnhandledRejections
を利用する。
(この方法だとエラーが捕捉できない)
javascript - Possibly unhandled rejection in Angular 1.6 - Stack Overflow
app.config($qProvider => { $qProvider.errorOnUnhandledRejections(false); })