はまったりひらめいたり…とか…

Angularや.NETやAzureやその他色々。

Angular Elementsで作成したWeb Componentsの設定情報をプレーンなJSから変更する

はじめに

この記事内のアプリケーションは下記バージョンで構成しています。

  • Angular CLI: 10.0.1
  • Angular: 10.0.2

この記事を参照されるタイミングによっては

サンプルコードが動作しなかったりする可能性がありますのでご留意くださいませ。

そもそもなにをしたいのか

Microsoft GraphはGraph ToolkitというWeb Componentsを公開していまして

オリジナルでそれを作ってみたいなー。というのが発端です。

今回の記事はその前哨戦。

Web Componentsを作ってみてハマった箇所の備忘録。といった感じですね。

Angular Elements

Angularを使用している場合は、Angularで作成したComponentをWebComponentsでパッケージングしてくれる

Angular Elementsというものが存在します。Angularで諸々なれている自分はこれを使うのが一番手っ取り早そうです。

基本的な使い方などは、👆のドキュメントを参考にすれば良いと思うので割愛します。

最終的に下記のようにAngularを使用していないようなプレーンなHTMLで

作成したWebComponentsのセレクタを指定してAngularで作成したComponentを表示させることが出来ます

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
</body>
</html>

👇

f:id:TakasDev:20200705111959p:plain

情報を設定する🤔

さて、Micorosoft Graphにアクセスするということは、ADのアプリケーション情報などが必要になります。

設定情報をWeb Components作成時点でビルドインしても良いのですが、それでは使い回しがしにくいです。

具体的には下図のような構成にしたいところですね。

f:id:TakasDev:20200705113551p:plain

ビルドされたソースはどうなっている?

ビルドされたパッケージ内のServiceクラスに値を直接外部から突っ込めるでしょうか?

下記のTSがビルドされたソースを整形して見てみます。

export class LibSettingsComponent implements OnInit {
...
 constructor(
    private service: LibStateService
  ) { }
...
  set setting(setting: { state: string, userId: string, userName: string }) {
    console.log('emit');
    this.service.setting(setting);
    console.log(this.service.state);
    this.dummyStSet = 'emit';
  }
}
        Oy), By = ((Iy = function() {
            function e(t) {
                _classCallCheck(this, e),
                this.service = t,
                this.dummySt = "dd"
            }
            return _createClass(e, [{
                key: "ngOnInit",
                value: function() {}
            }, {
            ...
            }, {
                key: "setting",
                set: function(e) {
                    console.log("emit"),
                    this.service.setting(e),
                    console.log(this.service.state),
                    this.dummyStSet = "emit"
                }
            }]),

this.serviceはfunctionの引数のtで更にそこの呼び出し元の…?

いずれにせよServiceのインスタンスに直接アクションをするのは多大な労力を伴いそうです。

設定用のComponentを用意する

Componentからのアクセスは簡易に行えます。なので設定用のComponentを作成する。というアプローチをとってみます。

中身が空の設定用のComponentを用意します。

設定用のComponentは@InputでSetterのみを提供し、Setterの実装内で設定情報Serviceを書き換えます。

具体的なソースコードは下記のとおりです。

色々視認やデバッグしやすいようにHTMLテンプレートにものを突っ込んでますが空で問題ないと思います。

import { Component, OnInit, Input } from '@angular/core';
import { LibStateService } from '../../services/lib-state.service';

@Component({
  selector: 'scl-lib-settings',
  template: '<div>{{dummySt}}</div>'
})
export class LibSettingsComponent {

  dummySt = 'bf';

  constructor(
    private service: LibStateService
  ) { }

  @Input()
  set setting(setting: { state: string, userId: string, userName: string }) {
    // ここで設定情報をServiceに反映する
    this.service.setting(setting);
    this.dummySt = 'emit';
  }
}

これでプレーンなHTMLのタグ上などで、settingプロパティに値を突っ込むことで設定情報が反映されるようになりました。

設定用のコンポーネントタグを書かなくて良いようにする

さて、設定を外から与えることができるようになりましたが

何も情報を出力しない設定用のコンポーネントをHTML上に書くのはなんともダサ味があります。

なので、スクリプトでも設定情報の反映ができるようにしたいところです。

そこで、

  1. 設定用のコンポーネントタグをcreateElementして
  2. 設定を行い
  3. 設定用のコンポーネントタグを削除する

こんな感じの設定スクリプトをAngular Elementsから提供することにします。

設定スクリプトは下記のようなものです。

class LibSettings {

    setSettings(data) {
        const settingDom = document.createElement('scl-lib-settings');
        document.body.appendChild(settingDom);
        settingDom.dummySt = 'hoge';
        settingDom.setting = data;
        document.body.removeChild(settingDom);
    }
}

作成した設定スクリプトをWebComponentsをビルドする際に一緒に吐き出したいので、angular.jsonをいじります。

    "custom-element": { // Angular Elementsのプロジェクト名
      ...
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            ...
            "scripts": [
               // 👇を追加
              "projects/custom-element/src/static-scripts/lib-settings.js"
            ]

これで、WebComponentsをパッケージングしたときに、同一ディレクトリに設定スクリプトも吐かれるようになりました。

f:id:TakasDev:20200705121442p:plain

使ってみる

設定スクリプトで設定が反映されたサービスのプロパティをアラート表示するボタンComponentを作成しました。

このボタンを押下することで設定情報の反映の確認を行ってみます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <link rel="stylesheet" href="../../dist/custom-element/styles.css">
    <script src="../../dist/custom-element/custom-element.js"></script>
</head>
<body>

    <h1>Web Component (Costum Elements)</h1>
    <scl-custom-button></scl-custom-button>
    <scl-sample-form-group></scl-sample-form-group>
    <scl-router-page></scl-router-page>
    <script src="../../dist/custom-element/scripts.js"></script>
    <script type="text/javascript">
        // 設定スクリプトを使用して設定情報の反映
        const cl = new LibSettings();
        const setting = { state: 'HTMLから設定したよ!', userId: 'user', userName: 'hogehoge' }
        cl.setSettings(setting);
        //
    </script>
</body>
</html>

上記の状態で、アラートに「HTMLから設定したよ!」と表示されればOKです。

f:id:TakasDev:20200705122727g:plain

設定できました。

これで設定情報を外部から与えることができそうです。

おわりに

長い前哨戦が終わったので、次回はようやく目的だったオリジナルのGraph Toolkitの作成に入ってみます。

手を付けれていないのでいつになるかわかりませんが

今回検証を行ったソースコードは下記となります。

github.com

色々検証で試行錯誤したので少々とっちらかっているのはご容赦くださいませ…

Microsoft MVPを初受賞しました

タイトルの通りMicrosoft MVPを初受賞しました。

カテゴリはOffice Developmentとなります。

登壇の機会を多くくださった.NETラボの皆様

セッションを聞いてくださった皆様

フィードバックをくださった皆様

個人の活動であるにも関わらず会社でぼくの活動をサポートしてくださった皆様

僕の活動にあらゆる形で関わってくださった皆様にお礼申し上げます。

本当にありがとうございました。

これからも楽しんで技術を追いかけ続けようと思います。

ただ看板を頂いた以上はそれに恥じない動きを心がけたいと思います。

感想

初受賞ということもあり、Twitterで感想おさまらんなー。というわけで感想をポストしています。

Officeの名を冠するカテゴリで受賞させていただいたのですが

僕のブログは「Office」で検索しても1件も引っ掛からないレベルでOfficeに触れていません。

f:id:TakasDev:20200703202650p:plain

(1件くらいあるやろと思ってたので「マジか」と自分でも思いました。。。)

  • よく言えばMicrosoft Graph特化で
  • OfficeというよりOfficeのデータに熱中していて
  • Webや.NETといったフロント方面から執拗にOfficeのデータにアプローチを行っている

そんな様が審査時に「変な人」的に印象にのこったかもしれないなーとか思ったりしています。

今後の話

受賞したからには継続して受賞していきたいと思いますし

Azureが好きで、Angularが好きで、.NET Coreが好きで、Microsoft Graphが好き。といった状況もまだまだ続くと思います。

そんな自分なので、Microsoft Graphはもちろんですが、Microsoft Graph以外も含め、多くの情報発信ができれば良いな。と考えていますので

今後ともよろしくおねがいいたします🙇

蛇足

受賞メールが届いた日は興奮したのか一睡もできず、かなり久々に完徹して出勤する事になりました。

結果、お昼すぎに睡魔に勝てず途中リタイア。

自分の老いにも確かな手応えを感じてしまったので、体調にも気をつけつつ頑張ろうと思います。。

AzureDevOpsのCIで.NET CoreとAngularのアプリケーションをバージョンアップする

はじめに

この記事内で使用する各フレームは下記バージョンで構成しています。

  • Angular: 9.1.8
  • .NET Core: 3.1

また、AzureDevOpsは記事作成時点のキャプチャやコマンドが掲載されています。

この記事を参照されるタイミングによっては

動作しなかったり画面が違うといった可能性がありますのでご留意くださいませ。

モチベーション

アプリケーションのバージョンアップ作業って地味で忘れがちです。

そういった作業は自動で行ってしまいたい!!

と、いうわけでAzure DevOpsのCIフローの中でバージョンアップ作業ができないか試してみました。

僕が主に使用しているのは.NET CoreとAngularのアプリケーションですので

その二つのアプリケーションでバージョンアップを実施するCIを組んでみようと思います。

.NETアプリケーション

.NETのアプリケーションのバージョンアップをCIで実現しようと思います。

.NETアプリケーションのバージョン番号はメジャー.マイナー.ビルド番号.リビジョンで表現されますが

ビルド番号、あるいはリビジョンに*指定で、ビルド時に自動的にバージョンアップをしてくれます。

なので普通に運用する場合は何も考えなくてもバージョンアップはしてくれるのですが

バージョン付番を独自のルールで運用したり、付番したバージョンをデータストアに格納したりとかしたい場合があります。

なので、今回は独自ルールでバージョンを付番してアプリケーションを発行するCIを構築してみようと思います。

バージョンアップルール

今回は下記のルールでバージョンアップしようと思います

  • 2020年6月2日からの経過日数をバージョン番号とする
  • ビルドを行った日の00:00からの経過分数をリビジョンとする

.NETアプリケーションのビルドコマンドでバージョン番号を指定できるよう準備

MsBuld.exeを使用してビルドする際のコマンドでカスタムプロパティを指定できます。

参考:MSBuild Properties1

そこで、ビルドコマンドのArgumentsでバージョンを変更できるようにします。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UseWPF>true</UseWPF>
    <PublishSingleFile>true</PublishSingleFile>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
    <!-- バージョン番号のベースの経過日数を受け取る -->
    <BuildNumber Condition=" '$(BuildNumber)' == '' ">0</BuildNumber>
    <!-- リビジョンのベースの経過分数を受け取る -->
    <Revision Condition=" '$(Revision)' == '' ">0</Revision>
    <!-- バージョンの指定-->
    <AssemblyVersion>0.0.$(BuildNumber).$(Revision)</AssemblyVersion>
    <VersionPrefix>0.0.$(BuildNumber).$(Revision)</VersionPrefix>
  </PropertyGroup>

</Project>

これで、MsBuild.exe <プロジェクト> /p:BuildNumber=<適当な数字> /p:Revision=<適当な数字>でバージョンをコマンド上で指定できるようになりました。

MsBuild.exe <プロジェクト> /p:BuildNumber=1 /p:Revision=2した結果は下図のとおりです

f:id:TakasDev:20200613115738p:plain

CIの設定

CIで使用する変数の設定

CIの複数のタスクでバージョン情報を使い回すので、その情報を格納する変数を指定します。

経過日数を格納する$daysと経過分数を格納する$dayintervalを用意しています。

variables:
  days: 0
  dayinterval: 0

Power Shell

まずはPowerShellでコマンドで指定するバージョン番号とリビジョンを生成しています。

参考:PowerShellタスク

現在日付をゴニョゴニョして、$days$dayinterval 変数に格納します。

  • 2020年6月2日からの経過日数をバージョン番号とする
  • ビルドを行った日の00:00からの経過分数をリビジョンとする

ってことをやっています。

- task: PowerShell@2
  displayName: 'Calc Build Version'
  inputs:
      targetType: inline
      script: |
        $baseDate = [datetime]"06/02/2020"
        $currentDate = $(Get-Date)
        $interval = NEW-TIMESPAN -Start $baseDate -End $currentDate
        $days = $interval.Days
        Write-Host "##vso[task.setvariable variable=days]$days"
        echo $days
        $today = Get-Date -f d
        $startdate = Get-Date $today
        $todayinterval = NEW-TIMESPAN -Start $startdate -End $currentDate
        $dayinterval = [Math]::Truncate($todayinterval.TotalMinutes)
        Write-Host "##vso[task.setvariable variable=dayinterval]$dayinterval"
        echo $dayinterval

Write-Host "##vso[task.setvariable variable=<変数名>]<PowerShell内の変数(突っ込みたいValue)>"で👆で作成した変数に突っ込めます。

このPowershellでは、👆で指定したdaysdayintervalに値を突っ込んでいますね。

バージョンの情報を管理しているサービスなんかがある場合は

WebAPIを作成して、Invoke-WebRequestコマンドを使用して引っ張ってきたり登録したりすればいいかなと思います。

MsBuild

MsBuildで👆で指定したバージョンを指定してビルドを行うコマンドを作成していきます。

参考:MsBuildタスク

- task: MSBuild@1
  inputs:
    solution: '**/<プロジェクト名>/**/*.csproj'
    configuration: Release
    msbuildArguments: /t:Publish /p:BuildNumber=$(days) /p:Revision=$(dayinterval)

これはそこまで複雑なことはしていないですね。

PowerShellで設定した変数をMsBuildで使用するカスタムプロパティに指定しています。

実行結果

PowerShellタスクで指定するバージョン情報を出力しています。

f:id:TakasDev:20200613134513p:plain

Publishされたファイルのバージョンが、Powershellで生成した情報で構成されていることが確認できました。

Angularアプリケーション

普通にWebアプリケーションを作成するだけであれば特に意識する必要はないと思うのですが

ライブラリを作成してPublicなりPrivateなnpmレジストリにPublishしたい場合、バージョンアップは必要な作業です。

基本はnpm versionコマンドでバージョンアップしていけば良いかなと思っています。

バージョンアップルール

今回は下記のルールでバージョンアップしようと思います

  • masterブランチのCIはnpm version patchでパッチバージョンを上げる
  • developブランチのCIのときはプレリリースでdevバージョンとしてリリースする

CIの設定

ちょっとCIの順番と連動していないのですが、行う作業の関連など考慮して順不同で説明しています。

Power Shell

npmコマンドをPowerShellで実行します。

このPowershellコマンドでは下記のことを実行します

  • npm versionの実行
    • ライブラリのpackage.jsonのバージョンを上げたいのでライブラリのディレクトリで行う
  • バージョン変更をリモートリポジトリにPushする
    • そうしないと次回以降も同一のバージョンが生成されてしまうためですね

参考: MS Doc - Run Git commands in a script

参考: MS Doc - Build Azure Repos Git or TFS Git repositories

参考: Stack Overflow - How to increase a version of an npm package using Azure Devops pipeline

先にyamlを掲載すると👇な感じになります。

- task: PowerShell@2
  displayName: 'Version Up & Commit'
  inputs:
    targetType: inline
    script: |
      git config user.name "learn-angular-library-ci Build Service (devtakas-public)"
      git config user.email "dummy@example.com"
      $BranchName = "$(Build.SourceBranch)" -replace "refs/heads/"
      git checkout $BranchName --quiet
      cd projects\sample-lib
      npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent
      git add .
      git commit -m "$env:VERSIONCOMMENT"
      git push --quiet

Script内1~2行目

git config user.name "learn-angular-library-ci Build Service (devtakas-public)"
git config user.email "dummy@example.com"

gitにコミットするためにユーザーの情報などを設定しています。

ここは好きな値で問題ないです。

Script内3~4行目

$BranchName = "$(Build.SourceBranch)" -replace "refs/heads/"
git checkout $BranchName --quiet

Azure DevOpsのCI実行ログを見ればわかるのですが

ソースコードの操作が行われているブランチは厳密にはCIコマンドで指定したブランチと異なります。

f:id:TakasDev:20200614140458p:plain

バージョン変更を行ったコミットをPushしたいブランチはCIで指定するブランチなので

環境変数に格納されている作業ブランチをベースに作業ブランチのチェックアウトを行う必要があります。

Script内最後の行まで

cd projects\sample-lib
npm version $env:VERSIONCOMMAND --preid=dev -m "$env:VERSIONCOMMENT" --force --silent
git add .
git commit -m "$env:VERSIONCOMMENT"
git push --quiet

作業ディレクトリをライブラリのpackage.jsonがあるところまで移動して諸々作業します。

$env:VERSIONCOMMAND はAzure DevOpsのCI上で設定する変数です。

f:id:TakasDev:20200614141100p:plain

masterのCIではpatch、developのCIではprereleaseを指定しています。

AzureDevOpsのCIでは、rootディレクトリ以外ではnpm versionしてもCommitが発生しなかったので

別途 git add .git commitしています。

警告などでるとAzure DevOpsのCIでエラーを吐いてしまうので

適宜--quiet--silentでエラーにならないように回避しています。

参考: Stack Overflow - How to generate NPM release candidate version

参考: はらへり日記 - npm scriptsでエラーログを表示させたくない話

認証の設定

参考: MS Doc - Run Git commands in a script

AzureDevOpsで管理されているリポジトリに対してPushを行いたいので

ソースを取得する段階で認証情報を設定しちゃいます。

下記で設定可能です。

steps:
- checkout: self
  persistCredentials: true

Trigger

例えばdevelopブランチの変更があった場合、developブランチにCommit/Pushが行われます。

つまり、なにも考慮しないと無限ループになります(なった)。

自分は、単純に下記の通り自動変更される対象のファイルをexclude設定にしました。

trigger:
  branches:
    include:
    - develop
  paths:
    exclude:
    - projects/sample-lib/package.json

ここは人により設定が変わりそうなところですね。

ユーザーの設定

CIには実行するユーザーが割り振られています。

なのでそのユーザーがソースコードに対してCommitしたりPushできたりするように設定する必要があります。

参考: MS Doc - Run Git commands in a script

設定場所は下図から確認できます。

f:id:TakasDev:20200614142736p:plain

Usersの<プロジェクト名> Build Serviceという名前に基本なっていると思います。

設定は下記のとおりですね。

f:id:TakasDev:20200614143016p:plain

  • Contribute / Read
    • 必須
  • Create Branch / Create Tag
    • 必要があれば
    • リリース時にタグ付けしたいとか、自動コミットじゃなくてブランチとPR作りたいとかの場合かな?と思います
  • Bypass policies when pushing
    • ブランチポリシーで保護しているブランチに対してPushしたい時に必要です。

実行結果

今回はDevOps上のArtifactsにライブラリをPublishしました。

  • developブランチのCIの場合

f:id:TakasDev:20200614144429p:plain

preleaseで出力されていることが確認できます。

  • masterブランチのCIの場合

f:id:TakasDev:20200614145048p:plain

patchで出力されていることが確認できます。

おわりに

今回使用したリポジトリは下記の通りです。

Angularの方はブランチポリシー下の動作検証のためAzure DevOpsでソース管理もしています。

.NET Core

Angular

供養

長く苦しい戦いの記録

f:id:TakasDev:20200614150251p:plain

Angular MaterialのComponentHarnessを使ってみた(Autocomplete編)

はじめに

この投稿内のAngularは下記のバージョンで構成しています。

この記事を参照されるタイミングによってはサンプルコードが動作しない可能性がありますのでご留意くださいませ。

  • Angular CLI: 9.1.6
  • @angular/material: 9.2.3

ComponentHarness

先日、ng-japan OnAir #16 を視聴した際にでてきた

Angular Materialで提供されているComponentHarnessを使ってみました。

自分が作るAngularのアプリケーションはMaterialを多用しているものが多く

Componentのテストが壊れにくくなりそうである。ということが魅力的でした。

ComponentHarness含む「CDK Testing API」については

下記の@lacolacoさんのレポートをご参考いただければと思います。

hackmd.io

AutocompleteのComponentHarness

割とMaterial全般を使っているので、一個ずつComponentを確認しようかな、と思いまして。

まずは「Autocomplete」に手を付けてみました。

(構成の都合上MatInputもちょいちょい入ってますが…)

material.angular.io

超絶雑ですが👇がテストを行うComponentの構成です。

f:id:TakasDev:20200516174532p:plain

Serviceからリストを受け取ってAutocompleteでリスト表示。

チェックボックスでAutocompleteコントロールのDisbaledを制御できる。みたいな感じです。

Harnessの作成

使用するのは初めてなので、そもそもHarnessの作り方、使い方から確認していきます。

Angular Materialのドキュメントと、ng-japan OnAirで実装されていたソースなどを参考に実装しました。

Componentテスト用のHarnessクラスを作成し、そこに諸々準備していきます。

MaterialのComponentHarnessは、各Materialのtesting内に格納されているそうです。 あとはMatInputと組み合わせて使用するので、MatInputのHarnessも必要そうです。

import { MatAutocompleteHarness } from '@angular/material/autocomplete/testing';
import { MatInputHarness } from '@angular/material/input/testing';

で、AutocompleteとMatInputのHarnessをインポートしておきます。

privateでlocatorを準備して、コントロールにアクセスできるようにします。

今回、プレーンなチェックボックスも使用するので、locatorは👇な感じになりました。

  private getCheckBoxLocator = this.locatorFor('[data-testctl="checkbox"]');

  private getIncputLocator = this.locatorFor(
    MatInputHarness.with({
        placeholder: 'Test',
    })
  );

  private getAutoCompleteLocator = this.locatorFor(MatAutocompleteHarness);
  • チェックボックスのコントロールはテスト用の属性を指定して取得
  • MatInputは、MatInputHarnessを介して、placeholderが「Test」になっているコントロールを取得
  • AutocompleteはComponentに一つしかないので「MatAutocompleteHarness」のみで取得

コントロールの動作を実装する

Harnessクラスの中で公開する、コントロールの動作を実装していきます。

例えば、Dsiableを制御するチェックボックスのトグルは👇な感じですね。

  async toggleCheckBox(): Promise<void> {
    const chkbox = await this.getCheckBoxLocator();
    await chkbox.click();
  }

Locatorを介して取得したオブジェクトのClickイベントを叩くだけです。かんたん。

Disable状態を変更する動作をしたあとは、実際のコントロールの状態を確認したいです。

  async inputControlDisabledState(): Promise<boolean> {
    const input = await this.getIncputLocator();
    return input.isDisabled();
  }

こっちはlocatorを介してInputのオブジェクトを取得し、そのオブジェクトのisDisbaledステータスを返却します。

こっちもかんたん。

テストを実装してみる

さて、specファイルでテストを記述していきます。

テストで先程作成したHarnessを使用するためにfixtureを作成したあとに

使用するHarnessをFixtureを関連付けます(fixtureにharness(補助具)を割り当てるという表現になるんですかね?)。

  beforeEach(async () => {
    fixture = TestBed.createComponent(テストするComponent);
    component = fixture.componentInstance;
    harness = await TestbedHarnessEnvironment.harnessForFixture(
      fixture,
      作成したHarnessクラス
    );
    fixture.detectChanges();
  });

これで準備完了です。

行うテストを実装していきます。

まずは「チェックボックスをクリックしたらAutocompleteのInputが入力不可になること」を実装してみます。

  it ('CheckBoxToggle -> Input is Disabled', async () => {
    await harness.toggleCheckBox(); // Harness上で実行されるクリック処理
    const isDisabled = await harness.inputControlDisabledState; // Harness上で取得されるInputのDisabled状態
    expect(isDisabled).toBeTruthy();
  });

なんぞこれ?ですね。めちゃくちゃ簡単になっています。

Harnessなしの状態だと、Specファイル内でやっていた、ControlをElementから取得してー。とか。

そのコントロールから値を読んでー。といった処理が一掃されています。

そういったコードが抹消されたことにより、テストコードの可読性も上がっていますね。

さて、本題です。

AutocompleteのComponentHarness(2)

Autocompleteのコントロールでは下記3点をテストしてみようと思います。

  • Autocompleteのリストの初期状態
  • 対応するInputに入力があった場合に絞り込まれた状態となるか
  • Autocompleteのリストから選択された場合は対応するInputに文字列が入力されるか

まぁ、Materialで担保している内容もありますが、勉強なので…ということで。

さっそくハマる

リストの状態は、下記で取得できるのかいなと思っていたのですが

結果として帰ってきたのは空配列でした。

  async getAutoCompleteList(): Promise<MatOptionHarness[]> {
    const ctrl = await this.getAutoCompleteLocator();
    return await ctrl.getOptions();
  }

実装を確認する

ひとまず、困ったときは実装を確認するのみです。

実装の細かい部分はさておき、getOptions()の動きとしてはMatOptionのオブジェクトを取得してきているようです。

では、表示されるオプションはどうなっているのか確認してみます。

実際に動作させてElementを参照してみるとわかるのですが

AutoCompleteのリストはフォーカスがInputにあたった際に生えてくるもののようです。

f:id:TakasDev:20200516183232p:plain
リストが表示されているとき

f:id:TakasDev:20200516183316p:plain
リストが表示されていないとき

なので、オプションの情報を読むためには、「フォーカスをインプットに当てる」というワンアクションが必要であると予測します。

「フォーカスをインプットに当てる」処理を実装する

うまく行かなかった方法

インプットのlocatorはすでに作成したので、こいつが使えないかと思いました。

実装すると👇な感じですね。

  async inputFocus(): Promise<void> {
    const input = await this.getIncputLocator();
    await input.focus(); // focusを当てる
  }

これを、オプション情報の取得前に実行します。

  async getAutoCompleteList(): Promise<MatOptionHarness[]> {
    const ctrl = await this.getAutoCompleteLocator();
    this.inputFocus();
    return await ctrl.getOptions();
  }

結果、うまく行きませんでした。

f:id:TakasDev:20200516184201p:plain
テスト結果

フォーカスはあたっているが…!って感じですね。Autocompleteのリストが表示されていません。

ただ、デバッグを行っているウィンドウを選択していたらオプションが表示されるのでテストは通るのです。

f:id:TakasDev:20200516184534p:plain
デバッグウィンドウアクティブ時のテスト結果

結果はともかく、方針としては問題ないようです。

先人にならう

そもそも、MaterialのAutocompleteのComponent自体は同じようなテストをしているはずです。

どのようなテストを行っているのか確認してみました。

実装を見てみると、フォーカスをあてる処理としてdispatchFakeEvent(input, 'focusin');なることをしているようです。

ひとまず、dispatchFakeEvent周りの実装(この子この子この子)はそのまま使用させていただこうかと思います。

dispatchFakeEventに食わせるElementの取得

dispatchFakeEventの引数としてHtmlElementを要求されているので、その情報がほしいです。

locatorからElementの情報を取得できるので、それが利用できないか試してみました。

  async getAutoCompleteList(): Promise<MatOptionHarness[]> {
    const ctrl = await this.getAutoCompleteLocator();
    const inputElement: any = await ctrl.host(); // Element取得
    dispatchFakeEvent(inputElement, 'focusin'); // focus当てる
    return await ctrl.getOptions();
  }

しかし、host()の情報を利用するのはだめでした。

Elementの情報は取得できるのですが、HtmlElementそのものではないようで、dispatchEventが参照できずに終了します。

f:id:TakasDev:20200516190250p:plain

ここは本家よろしく、fixtureから直接取得する他なさそうです。

苦肉の策

fixtureから直接取得することにはしましたが、なるたけspecの方に実装したくはありません。

フォーカスを当てるのは、あくまでAutocompleteのリストを表示させてその値を参照したいからであって

そういった値を取得するまでのあれやこれやの処理はHarness側に寄せたいと思ったためです。

なので👇のように実装しました。

  async getAutoCompleteList(fixture: ComponentFixture<AutoCompleteComponent>): Promise<MatOptionHarness[]> {
    const ctrl = await this.getAutoCompleteLocator();
    const inputElement = fixture.debugElement.query(By.css('[data-testctl="autcompinput"]')).nativeElement;
    dispatchFakeEvent(inputElement, 'focusin');
    return await ctrl.getOptions();
  }

fixtureを引数でもらって、そこからElementを取得する力技です。

ベストプラクティスからは遠いかな?とも思いますが、これでうまくいくかどうか試してみます。

f:id:TakasDev:20200516224139p:plain

うまくいきました。

テストの実装はどうなったか

では、テスト本体の実装はどうなったのか見てみます。

テスト実行部分だけ抜粋します。

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it ('init option list', async () => {
    const datas = await harness.getAutoCompleteList(fixture);
    expect(datas.length).toEqual(10);
  });

  it ('filter option list', async () => {
    await harness.inputText('0');
    fixture.detectChanges();
    const datas = await harness.getAutoCompleteList(fixture);
    expect(datas.length).toEqual(1);
  });

  it ('option selection', async () => {
    await harness.inputText('');
    fixture.detectChanges();
    await harness.selectOption('0さん', fixture);
    const iputValue = await harness.inputTextValue();
    expect(iputValue).toEqual('0さん');
  });

  it ('CheckBoxToggle -> Input is Disabled', async () => {
    await harness.toggleCheckBox();
    const isDisabled = await harness.inputControlDisabledState;
    expect(isDisabled).toBeTruthy();
  });

ものすごくスッキリしました。

改めて見て「これがAngularのComponentのテストコードってマジ?」とか思いました。

まとめ

Angular MaterialのAutocompleteのComponentHarnessの使い方を見てきました。

実装した感覚、コード自体は普段と比べて書く量はそこまで変わらないような印象ですが

テストコード本体のスッキリ具合や

意図せずともテストの動作を個別に実装することができるようになるので

ソースコードの再利用性や可読性はぐっと上がるのではないかと思いました。

(ひょっとしたら再利用性のほうが恩恵でかいのでは…?と思ってたりもします。)

MaterialのComponentHarnessは最初から色々とコントロールを動かすためのAPIを用意してくれているので積極的に使っていきたいですね!

今回作成した諸々は下記レポジトリに格納しています。

ちょいちょいMatrialのComponentHarnessで実験して、ハマったら記事に上げようかと思います。。

github.com

Azure AD(v2.0)の認証をWPF(.NET Core)とASP.NET Core WebAPIで使用しようとしてハマった話

はじめに

下記の構成で構築しています。

一部Previewを使用していることもあり

参照されるタイミングによっては掲載するソースコードで動作しない可能性がありますので

ご注意くださいませ。

やりたいこと

  • WPF(.NET Core製)アプリで認証してトークンを取得し、WebAPIと通信したい
  • AzureAD Application(v2.0)を使用して認証したい

基本そこまで難しいことはないというか、下記のドキュメントままなのですが

github.com

一部ハマったポイントがあるので備忘録的にメモを落としておきます。

1. AzureAD Application(v2.0)について

f:id:TakasDev:20200506154256p:plain

検証時点は個人アカウント(outlook)を使用するので👆画像を選択

この場合、ADアプリケーションのバージョンは強制的に2.0となります。

ここを設定しない場合は、Manifestの「accessTokenAcceptedVersion」を指定する必要があります。

f:id:TakasDev:20200506155823p:plain

2. トークン取得時に失敗する事象について

AuthenticationResult result = await _app.AcquireTokenInteractive(Scopes).ExecuteAsync();

アクセストークンの取得は上記の処理で行います。

しかし、このコードの実行の際に👇のエラーが発生しハマってしまいました。

AADSTS7000218: The request body must contain the following parameter: 'client_assertion' or 'client_secret'

暗黙フローを使用してトークンを取得するつもりだったので、シークレットを使用する余地もなく「?」な感じだったのですが

GitHubのIssueに同一のエラーが報告されていました。

ドキュメント曰く

Azure ADアプリケーションの「allowPublicClient」を「true」に変更すれば良いようです。

f:id:TakasDev:20200506160754p:plain

上記の設定を行うことで、認証とトークンの取得、トークンを使用したWebAPIへのアクセスが行えるようになりました。

最後に

今回作ってみたコードは下記となります。

github.com

AngularのライブラリをAzure Artifactsで使用する

はじめに

2020年1月時点の記事となります。

記事を見ていただくタイミングによっては

記事中で使用しているAzureのキャプチャやコマンドが変更されている可能性がありますのでご留意ください。

また、Angluarは下記バージョンで構成しています。

  • @angular/cli: 8.3.21
  • Angular: 8.2.14

同様に、コマンドなどが変更されている可能性がありますので、こちらもご留意ください。

やりたいこと

  • Angularでライブラリを作る。
  • そのライブラリをnpm installできるようにする。

プロジェクトが巨大かつ複数にまたがってくると

Angularに限らずライブラリとして分離したくなってくるのが開発者の性かもしれません。

そして作成したライブラリは既存のライブラリ同様npmやNuGetといったパッケージマネージャで管理したいところです。

ただ、社内だけでしか使わないようなものではありますしPublishに公開したくはない。

おまけに、npmの有料契約やってないし、verdaccioみたいなプライベートレジストリもってない。という状況です。

そこでAzure Artifactsを利用し、サクッとプライベートなレジストリを作りたいと思います。

Angularでライブラリを作る

ここはドキュメントにある通りです。

Sandboxの切り方など参考になったので、StackOverflowの回答も参考になるかとは思います。

Azure Artifats

新しいFeedの作成

早速ライブラリを公開するレジストリ(Feedと呼ばれているようです)を作っていきます。

Azure DevOpsの「Artifacts」を選択します。

f:id:TakasDev:20200105111707p:plain

一番最初は空の状態です。ここからFeedを作成していきます。

f:id:TakasDev:20200105111619p:plain

上図、Create Feedから新しいFeedを作成します。

f:id:TakasDev:20200105111957p:plain

そこまで複雑な設定画面ではありませんね。

  • FeedのURLで使用される名称
  • Visibity: Feedを使用できるユーザーの制御
  • UpstreamSourceの使用有無: npmjs.orgnuget.orgのパッケージも使用するか?設定

ちなみに、Artifactsを作成するProject自体のVisibilityがPublicになっている場合はAritefactsもPublic一択となります。

Feedへの接続

f:id:TakasDev:20200105112908p:plain

Connect to feedでFeedへの接続方法を確認できます。

f:id:TakasDev:20200105113111p:plain

今回はnpmを使用するのでnpmの接続方法を確認します。

他のパッケージレジストリを使用するときと同様に、プロジェクトに.npmrc ファイルを作成し

レジストリとして作成したAzure ArtifactsのFeedを指定していることがわかります。

vsts-npm-authの使用

PrivateなFeedのため、npm publishするためにはAzureDevOpsへの認証が必要となります。

WIndowsを使用している場合、認証に使用するトークンの取得が比較的かんたんに行えます。

npm install -g vsts-npm-authvsts-npm-auth -config .npmrcを行うだけですね。

ちょいと面倒なPersonalAccessTokenの作成から、なにからなにまでやってくれます。楽。

Publish

あとはnpmコマンドでPublishするだけです。

トークンの発行ができている場合は npm publish .\dist\[パッケージディレクトリ名] だけでFeedに発行されます。

※Angularのビルドコマンドで生成物がdist配下にできるので。

発行されたら、DevOpsのFeedの画面から下図のように確認できます。

f:id:TakasDev:20200105114949p:plain

赤枠内にパッケージのインストールコマンドもあるので、これを使用して作成したライブラリのインストールを行えます。

Feedを使う

Feedを使用する場合、前述の「Feedへの接続」と同じことをしてあげる必要があります。

レジストリのURLを指定したnpmrcファイルの作成と(場合によっては)トークンの取得です。

その設定だけで、npm installで作成したライブラリのインストールが行えるようになります。

他のライブラリ使用時にエラーとなる場合

例えば、npm install @angular/animation などnpmjs.orgで管理されているパッケージを使用しようとした場合、404エラーとなることがあります。

これはUpstreamが正しく設定されておらず、npmjs.orgのパッケージを見に行けていないことが原因であると考えられます。

VisiblityをPublicからPrivateに手動で変えたときなどにそのような状況になるようです。

f:id:TakasDev:20200105115903p:plain

FeedSettingsからUpstreamの設定を行います。

f:id:TakasDev:20200105120014p:plain

上図の通りnpmjs.orgが存在すれば404は発生しないはずです。

Azure Pipelinesでライブラリをデリバリする

一々ローカルでnpm publishするのは面倒なのでCIでPublishされるようにしたいところです。

そこで、使用しているAzure Artifactsと同じプロジェクトでCIパイプランを構築します。

といってもそこまで複雑なことはなく、npmAuthenticate タスクを実行の後

npm publishするだけです。同一のプロジェクトだからかな?非常にかんたんです。

- task: npmAuthenticate@0
  inputs:
    workingFile: '.npmrc'

- script: |
    npm install -g @angular/cli
    npm install
    npm run build:lib
    npm publish .\\dist\\test-lib
  displayName: 'npm install ,build and publish'

f:id:TakasDev:20200105121506p:plain

これで、ライブラリのCIも構築できました。

おわりに

「Angularの」とありましたがほぼAzure Artfactsの使い方ですね。。。

プライベートなパッケージ管理をしたい場合でAzureDevOpsを使用している場合

別途の契約や、自前のサーバーを用意する必要がないので、Azure Aritfactsは非常に効果的な手なのかもしれません。

どんどん活用していきたいです。

👇今回検証で使用したリポジトリです。

github.com

参考文献

Set up your client's npmrc

フロントエンドのMicrosoft Graphことはじめ

はじめに

使用するライブラリは下記バージョンとなります。

また、Graph使用部分はAngularになるべく依存しない形で構成していますが

手を抜きたい箇所がちょこちょことあるのでベースは下記Ver.のAngularで作成しています。

  • @angular/cli: 8.3.0
  • Angular: 8.2.3

それぞれの内容について記事をご覧になるタイミングによっては、画面、仕様が変更されている可能性があるのでご留意くださいませ。

なにがしたいか

フロントエンドだけでMicrosoft Graphから情報取得したい。

以上。

実装

1.Azure AD Applicationの準備

例のごとく、Graphへのアクセストークン取得用のAzure AD Applicationを作成します。

今回はOutlookで管理しているメールの情報などを取得するため

Microsoft個人アカウントにアクセスできるアプリケーションを作成します。

ADアプリを作成する際に「個人アカウント」が使用できるタイプを選択します。

f:id:TakasDev:20190910223352p:plain

2.ライブラリのインストール

Httpの通信ベースでもGraphからは情報は取得できますが

.net同様公式からClientライブラリが提供されていますので、それを使用します。

インストールするライブラリは下記のとおりです。

msal.js

今回、個人アカウントの情報にアクセスするためAzureAdv2アプリケーションを使用します。

そのためADALではなくMSALを使用します。

Graphにアクセスする際に使用するアクセストークンの取得に使用します。

npm install msal --save

@microsoft/microsoft-graph-client

Microsoft Graphとやり取りすためのライブラリです。

npm install @microsoft/microsoft-graph-client --save

@microsoft/microsoft-graph-types

Clientには型情報がついてこないようです。

TypeScriptを使用している方は@microsoft/microsoft-graph-types もインストールしておくと

Graphから取得したデータのマッピングが幾分か楽になると思います。

後述するクエリオプションの$selectを使用するのであればいらないかなとは思います。

npm install @microsoft/microsoft-graph-types --save-dev

3. msal.jsのイニシャライズ

Graphからデータを取得するためには、何はともあれアクセストークンを取得する必要があります。

リダイレクト時に読み込まれるページ、あるいはリダイレクト時に読み込まれるJS等でmsalのイニシャライズを行います。

JSでClassを使用している場合はconstructorでイニシャライズを行います。

とにかくリダイレクト時にUserAgentApplicationが生成されるようにしておきます。

import * as msal from 'msal';

export class AuthService {
    private authClient: msal.UserAgentApplication;
    constructor() {
        this.authClient = new msal.UserAgentApplication({
            auth: {
                clientId: '<ADアプリのClientID>',
                authority: 'https://login.microsoftonline.com/common'
            }
       });
    }
}

UserAgentApplicationのコンストラクタの引数で、使用するAzureADアプリケーションの情報を与えます。

今回Microsoft個人アカウントの情報を取り扱いたいので、authorityhttps://login.microsoftonline.com/commonを指定します。

テナントに紐づく情報のみに抑えたい場合はhttps://login.microsoftonline.com/<tenant-id>でOKです。

4. GraphClientのイニシャライズ

先に記述したとおり、Graphからデータを取得するためには

AzureADアプリケーションから提供されるアクセストークンが必要となります。

GraphClientはイニシャライズ時に、Graphアクセス用のトークンを取得するauthProviderを設定することができます。

Providerを指定せずにGraphアクセス時にヘッダにアクセストークンを設定する事も可能ですが

アクセス都度ヘッダを設定する処理を書くのも邪魔くさいと思うのでProviderを設定するほうがいいかなと思います。

import * as graph from '@microsoft/microsoft-graph-client';

export class GraphService {
    private client: graph.Client;
    
    constructor() {
        this.client = graph.Client.init({
            authProvider: async (done) => {
                const token = await this.authService.acquireToken({ scopes: [ 'mail.read' ] });
                if (token) {
                    done(null, token.accessToken);
                } else {
                    done('Can not Get Token', null);
                }
            }
        });
    }
}

authProviderとして、通信を行う際のトークン取得処理を記述します。

3.で設定したmsal.jsのファンクションacquireTokenを使用しアクセストークンを取得します。

acquireTokenの引数で指定するスコープ情報は、使用したいリソースの権限情報をドキュメントで参照してください。

例えば、Messageの権限情報はこちらから参照できます。

acquireTokenはPromiseで値を返却してくれるので、async-awaitでアクセストークンを取得し

authProviderのCallbackdoneの引数にトークンを指定し実行します。

done(error: any, accessToken: string | null)を引数で実行されるCallbackです。

トークンが取得できなかったりエラーが発生した場合はaccessTokenをnullにし、errorにエラー情報を格納し

逆にトークンが取得できた場合はaccessTokenに取得したトークンを設定し、errorにnullを格納し実行します。

5. Graphの実行

下準備は整いました。GraphClientを使用し、自分に送信されたメールの情報を取得してみようと思います。

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages').get()
    }
}

処理を組み立てるときはMicrosoftのドキュメントにお世話になります。

例えばメッセージを一覧表示する - Microsoft Graph v1.0 | Microsoft Docs

.api()内のURLの指定は、この「HTTP要求」にあるアドレスを指定すればOKです。

と、いいますか、ページ下部にあるほぼそのまんまですね。

6. 複数ページにまたがるデータの取得

メールや予定表など、データ数がべらぼうに多そうなデータは一回のリクエストで全件データ取得できません。

あるいは自分で1回あたりの取得データの件数を絞ったりもできます。

アプリで Microsoft Graph データをページングする - Microsoft Graph | Microsoft Docs

その場合、@odata.nextLinkというプロパティに次ページのリンクURLが格納された状態で、Graphからデータが帰ってきます。

なので、全件データを取得したい場合は@odata.nextLinkに、次ページへのリンクが格納されなくなるまでループを回し続けなければならないというわけですね。

private messageList: Message[] = []; /* Message:@microsoft/microsoft-graph-types で提供されている型情報 */

// 初回は「/me/messages」で以降はnextlinkのURLが引数に
getMessage(url: string) {
    this.client.api(url).get().then(x => {
        (x.value as Message[]).forEach(element => {
            this.messageList.push(element);
        });
        if (x['@odata.nextLink'] !== '') {
            const nestNextLink = x['@odata.nextLink'];
            this.getMessage(nestNextLink);
        }
    });
}

7. クエリオプション

特定条件でのデータの絞り込み。取得データを特定の項目のみにする絞り込み等が行なえます。

何が行えるかは下記のドキュメントを参照してください。

クエリ パラメーターを使用して応答をカスタマイズする - Microsoft Graph | Microsoft Docs

その中からいくつかのクエリオプションを見てみます。

$select

取得する項目を指定する事ができます。

通信でどのような項目のやり取りされるかはAPIドキュメントを参照すればOKです。

今回使用してみた/messageのプロパティは下記のドキュメントで確認できます。

メッセージ リソースの種類 - Microsoft Graph v1.0 | Microsoft Docs

このプロパティから取得したい項目を選択することになります。

GraphClientでは.selectがそれにあたります。

下記のように.selectで取得したい項目名を指定します。

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages')
            .select('subject, receivedDateTime')
            .get()
    }
}

messageのメール表題のsubjectと受信日時のreceivedDateTimeを取得しています。

$filter

取得する項目の絞り込みを行うことができます。

$select同様、絞り込む項目はAPIドキュメントを参照します。

GraphClientでは.filterを下記のように使用します。

filterの論理演算子については下記のドキュメントを参照してください。

クエリ パラメーターを使用して応答をカスタマイズする - Microsoft Graph | Microsoft Docs

export class GraphService {
    private client: graph.Client;
    ....
    getMessage() {
        this.client.api('/me/messages')
            .filter('isRead+eq+false')
            .get()
    }
}

messageの内未読のもののみを取得しています。

まとめ

フロントエンドのみでGraphのデータにアクセスする方法を確認しました。

  • Graphへのアクセスは@microsoft/microsoft-graph-clientを便利に使用しましょう
  • Graphの使用の仕方は公式ドキュメントが一番参考になります
  • クエリオプションを使用してGraphを使いこなしましょう

今回検証に使用したサンプルは下記のリポジトリとなります。

github.com