かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その49 )( 入力画面3を作成する2 )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その48 )( 入力画面3を作成する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成
    • 「アンケート」の項目には、DB に専用のテーブルを作成して、そこに登録されたデータから動的に表示する処理を実装します。

参照したサイト・書籍

  1. Conditionally closing tag in Thymeleaf
    https://stackoverflow.com/questions/36747620/conditionally-closing-tag-in-thymeleaf

目次

  1. アンケートに表示する項目を定義するテーブルを Database Tools で作成する
  2. 作成した create table 文を Flyway 用の SQL ファイル V1__init.sql に追加する
  3. Doma-Gen で entity クラス, dao インターフェースを生成する
  4. SurveyOptionsHelper クラスを作成する
  5. SurveyOptionsHelper クラスのテストを作成する
  6. input03.html を修正する&動作確認

手順

アンケートに表示する項目を定義するテーブルを Database Tools で作成する

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その16 )( H2 Database に Flyway でテーブルを作成する ) を参考に、Tomcat を起動して Database Tools で in-memory モードの H2 Database に接続し、PUBLIC スキーマを選択してコンテキストメニューを表示した後、「New」-「Table」を選択します。

「Create New Table」ダイアログが表示されるので、以下の画像のようにテーブルを定義します。

f:id:ksby:20180418010743p:plain

ダイアログの下の「SQL Script」に生成された SQL は以下のようになります。

CREATE TABLE SURVEY_OPTIONS
(
    group_name VARCHAR(16) NOT NULL,
    item_value VARCHAR(1) NOT NULL,
    item_name VARCHAR(64) NOT NULL,
    item_order INT NOT NULL,
    CONSTRAINT PK_SURVEY_OPTIONS PRIMARY KEY (group_name, item_value)
);

「Execute」ボタンをクリックするとテーブルが作成されて、Database Tools 上に SURVEY_OPTIONS テーブルが表示されます。

f:id:ksby:20180418010940p:plain

作成した create table 文を Flyway 用の SQL ファイル V1__init.sql に追加する

作成した create table 文とデータを投入する insert 文を src/main/resources/db/migration/V1__init.sql の中に記述します。

CREATE TABLE INQUIRY_DATA
(
    ..........
);

CREATE TABLE SURVEY_OPTIONS
(
  group_name VARCHAR(16) NOT NULL,
  item_value VARCHAR(1) NOT NULL,
  item_name VARCHAR(64) NOT NULL,
  item_order INT NOT NULL,
  CONSTRAINT PK_SURVEY_OPTIONS PRIMARY KEY (group_name, item_value)
);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '1', '選択肢1だけ長くしてみる', 1);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '2', '選択肢2', 2);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '3', '選択肢3', 3);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '4', '選択肢4', 4);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '5', '選択肢5が少し長い', 5);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '6', '選択肢6', 6);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '7', '選択肢7', 7);
INSERT INTO SURVEY_OPTIONS VALUES ('survey', '8', '8', 8);

Tomcat を再起動すると SURVEY_OPTIONS テーブルが作成されてデータもセットされていることが確認できます。

f:id:ksby:20180418013514p:plain f:id:ksby:20180418013605p:plain

Doma-Gen で entity クラス, dao インターフェースを生成する

build.gradle の domaGen タスクを変更し、Tomcat を起動した状態で entity クラス, dao インターフェースを生成します。

build.gradle を以下のように変更します。

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.bootnpmgeb'
        def rootPackagePath = 'src/main/java/ksbysample/webapp/bootnpmgeb'
        def dbUrl = 'jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb'
        def dbUser = 'sa'
        def dbPassword = ''
        def tableNamePattern = 'SURVEY_OPTIONS'
        // おそらく変更不要なもの
        ..........
  • domaGen タスクの以下の点を変更します。
    • tableNamePattern.*SURVEY_OPTIONS に変更します。

Gradle Tool Window から domaGen タスクを実行します。"BUILD SUCCESSFUL" が表示されました。

f:id:ksby:20180423234132p:plain

Project Tool Window を見ると SurveyOptionsDao インターフェース、SurveyOptions クラスが作成されています。

f:id:ksby:20180423234422p:plain

domaGen タスクの tableNamePattern.* に戻します。

SurveyOptionsHelper クラスを作成する

SURVEY_OPTIONS テーブルのデータを取得して Thymeleaf テンプレートに出力するための SurveyOptionsHelper クラスを作成します。

まずは src/main/java/ksbysample/webapp/bootnpmgeb/dao/SurveyOptionsDao.java の以下の点を変更します。

@Dao
@ComponentAndAutowiredDomaConfig
public interface SurveyOptionsDao {

    /**
     * @param groupName groupName
     * @param itemValue itemValue
     * @return the SurveyOptions entity
     */
    @Select
    SurveyOptions selectById(String groupName, String itemValue);

    /**
     * 指定されたグループ名のレコードのリストを取得する
     *
     * @param groupName グループ名
     * @return {@SurveyOptions} エンティティのリスト
     */
    @Select
    List<SurveyOptions> selectByGroupName(String groupName);

    ..........
  • selectByGroupName メソッドを追加します。

src/main/resources/META-INF/ksbysample/webapp/bootnpmgeb/dao/SurveyOptionsDao/selectByGroupName.sql を新規作成し、以下の内容を記述します。

select
  /*%expand*/*
from
  SURVEY_OPTIONS
where
  GROUP_NAME = /* groupName */'survey'
order by
  ITEM_ORDER

ksbysample.webapp.bootnpmgeb.helper の下に db パッケージを新規作成します。その下に SurveyOptionsHelper.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.helper.db;

import ksbysample.webapp.bootnpmgeb.dao.SurveyOptionsDao;
import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;

/**
 * SURVEY_OPTIONS テーブルデータ取得用 Helper クラス
 */
@Component("soh")
public class SurveyOptionsHelper {

    private final SurveyOptionsDao surveyOptionsDao;

    /**
     * コンストラクタ
     *
     * @param surveyOptionsDao {@SurveyOptionsDao} オブジェクト
     */
    public SurveyOptionsHelper(SurveyOptionsDao surveyOptionsDao) {
        this.surveyOptionsDao = surveyOptionsDao;
    }

    /**
     * SURVEY_OPTIONS テーブルから指定されたグループ名のリストを取得する
     *
     * @param groupName グループ名
     * @return {@SurveyOptions} オブジェクトのリスト
     */
    public List<SurveyOptions> selectItemList(String groupName) {
        List<SurveyOptions> surveyOptionsList = surveyOptionsDao.selectByGroupName(groupName);
        if (CollectionUtils.isEmpty(surveyOptionsList)) {
            throw new IllegalArgumentException("指定されたグループ名のデータは登録されていません");
        }
        return surveyOptionsList;
    }

}

SurveyOptionsHelper クラスのテストを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/helper/db/SurveyOptionsHelper.java で Ctrl+Shift+T を押してコンテキストメニューを表示した後、「Create New Test...」を選択します。

src/test/groovy/ksbysample/webapp/bootnpmgeb/helper/db/SurveyOptionsHelperTest.groovy が新規作成されますので、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.helper.db

import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@SpringBootTest
class SurveyOptionsHelperTest extends Specification {

    @Autowired
    private SurveyOptionsHelper soh

    def "登録されているグループ名を指定してselectItemListメソッドを呼ぶとリストが取得できる"() {
        setup:
        List<SurveyOptions> surveyOptionsList = soh.selectItemList("survey")

        expect:
        surveyOptionsList.size() == 8
        surveyOptionsList[0].itemValue == "1"
        surveyOptionsList[0].itemName == "選択肢1だけ長くしてみる"
        surveyOptionsList[7].itemValue == "8"
        surveyOptionsList[7].itemName == "8"
    }

    def "登録されていないグループ名を指定してselectItemListメソッドを呼ぶとIllegalArgumentExceptionがthrowされる"() {
        when:
        List<SurveyOptions> surveyOptionsList = soh.selectItemList("notexists")

        then:
        def e = thrown(IllegalArgumentException)
        e.getMessage() == "指定されたグループ名のデータは登録されていません"
    }

}

テストを実行して成功することを確認します。

f:id:ksby:20180424005934p:plain

input03.html を修正する&動作確認

「アンケート」の項目を SurveyOptionsHelper クラスを利用して SURVEY_OPTIONS テーブルのデータを出力するように src/main/resources/templates/web/inquiry/input03.html を以下のように変更します。

            <div class="form-group" id="form-group-survey">
              <div class="control-label col-sm-2">
                <label class="float-label">アンケート</label>
              </div>
              <div class="col-sm-10" id="multiline-checkbox">
                <th:block th:each="surveyOptions,iterStat : ${@soh.selectItemList('survey')}">
                  <th:block th:if="${iterStat.index % 3 == 0}">
                  <div class="row"><div class="col-sm-12">
                    <div class="checkbox">
                  </th:block>

                  <label><input type="checkbox" name="survey" th:value="${surveyOptions.itemValue}">
                    <th:block th:text="${surveyOptions.itemName}">選択肢1だけ長くしてみる</th:block>
                  </label>

                  <th:block th:if="${iterStat.index % 3 == 2 || iterStat .last}">
                    </div>
                  </div></div>
                  </th:block>
                </th:block>
              </div>
            </div>

Tomcat を起動して入力画面3を表示してみましたが、ボタンの表示位置がおかしいですね。。。

f:id:ksby:20180425115119p:plain

html を見ると、最初の <th:block th:if="${iterStat.index % 3 == 0}">...</th:block> は正常に動作していますが、後の <th:block th:if="${iterStat.index % 3 == 2 || iterStat.last}"></div>...</th:block> が最後しか出力されておらず正常に動作していませんでした。</th:block> も最後に2つそのまま出力されています。どうも Thymeleaf では閉じタグ側を動的に出力させる書き方ができないようです。

            <div class="form-group" id="form-group-survey">
              <div class="control-label col-sm-2">
                <label class="float-label">アンケート</label>
              </div>
              <div class="col-sm-10" id="multiline-checkbox">
                  <div class="row"><div class="col-sm-12">
                    <div class="checkbox">
                  <label><input type="checkbox" name="survey" value="1">
                    選択肢1だけ長くしてみる
                  </label>
                  <label><input type="checkbox" name="survey" value="2">
                    選択肢2
                  </label>
                  <label><input type="checkbox" name="survey" value="3">
                    選択肢3
                  </label>

                  <div class="row"><div class="col-sm-12">
                    <div class="checkbox">
                  <label><input type="checkbox" name="survey" value="4">
                    選択肢4
                  </label>
                  <label><input type="checkbox" name="survey" value="5">
                    選択肢5が少し長い
                  </label>
                  <label><input type="checkbox" name="survey" value="6">
                    選択肢6
                  </label>

                  <div class="row"><div class="col-sm-12">
                    <div class="checkbox">
                  <label><input type="checkbox" name="survey" value="7">
                    選択肢7
                  </label>
                  <label><input type="checkbox" name="survey" value="8"></label>
                    </div>
                  </div></div>
                  </th:block>
                </th:block>
              </div>

stackoverflow で調べてみると Conditionally closing tag in Thymeleaf というページを見つけました。今回は th:utext を使用する方法に変更してみます。

src/main/resources/templates/web/inquiry/input03.html を以下のように変更します。

            <div class="form-group" id="form-group-survey">
              <div class="control-label col-sm-2">
                <label class="float-label">アンケート</label>
              </div>
              <div class="col-sm-10" id="multiline-checkbox">
                <th:block th:each="surveyOptions,iterStat : ${@soh.selectItemList('survey')}">
                  <th:block th:if="${iterStat.index % 3 == 0}"
                            th:utext="'&lt;div class=&quot;row&quot;&gt;&lt;div class=&quot;col-sm-12&quot;&gt;&lt;div class=&quot;checkbox&quot;&gt;'"/>

                  <label><input type="checkbox" name="survey" th:value="${surveyOptions.itemValue}">
                    <th:block th:text="${surveyOptions.itemName}">選択肢1だけ長くしてみる</th:block>
                  </label>

                  <th:block th:if="${iterStat.index % 3 == 2 || iterStat.last}"
                            th:utext="'&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;'"/>
                </th:block>
              </div>
            </div>
  • 上下の条件を満たした時に div タグを出力する処理を <th:block th:if="..." th:utext="..."/> の形式に変更します。
  • th:utext の中に書く HTML 文は <&lt; へ、>&gt; へ、&quot; へ変換します。

今度は画面も問題なく表示されました。

f:id:ksby:20180425122931p:plain

html を見てもきちんと閉じタグが出力されています。

            <div class="form-group" id="form-group-survey">
              <div class="control-label col-sm-2">
                <label class="float-label">アンケート</label>
              </div>
              <div class="col-sm-10" id="multiline-checkbox">
                  <div class="row"><div class="col-sm-12"><div class="checkbox">
                  <label><input type="checkbox" name="survey" value="1">
                    選択肢1だけ長くしてみる
                  </label>
                  <label><input type="checkbox" name="survey" value="2">
                    選択肢2
                  </label>
                  <label><input type="checkbox" name="survey" value="3">
                    選択肢3
                  </label>
                  </div></div></div>

                  <div class="row"><div class="col-sm-12"><div class="checkbox">
                  <label><input type="checkbox" name="survey" value="4">
                    選択肢4
                  </label>
                  <label><input type="checkbox" name="survey" value="5">
                    選択肢5が少し長い
                  </label>
                  <label><input type="checkbox" name="survey" value="6">
                    選択肢6
                  </label>
                  </div></div></div>

                  <div class="row"><div class="col-sm-12"><div class="checkbox">
                  <label><input type="checkbox" name="survey" value="7">
                    選択肢7
                  </label>
                  <label><input type="checkbox" name="survey" value="8"></label>
                  </div></div></div>
              </div>
            </div>

履歴

2018/04/25
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その48 )( 入力画面3を作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その47 )( Node.js を 8.9.1 → 8.9.4 へ、npm を 5.5.1 → 5.6.0 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成

参照したサイト・書籍

目次

  1. 入力画面3は何をする?
  2. 「お問い合わせの種類2」を必須にする
  3. 変換&入力チェック処理の仕様を決める
  4. お問い合わせの内容の textarea タグに maxlength 属性を追加する
  5. お問い合わせの種類1、お問い合わせの種類2の選択肢を表示する処理を追加する

手順

入力画面3は何をする?

久しぶりに画面の作成に戻ってきましたが、入力画面3で何をするつもりだったか忘れていました。。。 ということでまとめます。

  • 「お問い合わせの種類1」はドロップダウンリストの、「お問い合わせの種類2」はチェックボックスのサンプルです。選択肢は固定です(Enum で定義します)。
  • 「お問い合わせの内容」はテキストエリアのサンプルです。最大500文字の制限を付けます。
  • 「アンケート」は専用のテーブルを作成して、そこに登録されたデータから動的に表示します。
  • Spring Boot + npm + Geb で入力フォームを作ってテストする ( その9 )( 各画面の HTML を作成する2 ) で入力画面3を作成した時は「お問い合わせの種類1」「お問い合わせの内容」だけ必須にしていましたが、チェックボックスの必須チェックのサンプルも作成しておきたいので「お問い合わせの種類2」も必須に変更します。

「お問い合わせの種類2」を必須にする

src/main/resources/templates/web/inquiry/input03.html を以下のように変更します。

            <!-- お問い合わせの種類2 -->
            <div class="form-group" id="form-group-type2">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの種類2</label>
                <div class="label label-required">必須</div>
              </div>
  • <div class="label label-required">必須</div> を追加します。

画面は以下のようになります。

f:id:ksby:20180417004445p:plain

変換&入力チェック処理の仕様を決める

各項目で以下の変換&入力チェック処理を行います。

項目 変換&入力チェック処理
お問い合わせの種類1 必須チェック
お問い合わせの種類2 必須チェック
お問い合わせの内容 必須チェック
  • 「お問い合わせの内容」の最大文字数は maxlength 属性で制御されている前提とし、Javascript ではチェックしません(サーバ側では文字数チェックは行います)。

お問い合わせの内容の textarea タグに maxlength 属性を追加する

「お問い合わせ内容」の <textarea rows="5" name="inquiry" ...>maxlength="500" を追加します。

お問い合わせの種類1、お問い合わせの種類2の選択肢を表示する処理を追加する

まずは「お問い合わせの種類1」からです。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に Type1Values.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum Type1Values implements Values {

    PRODUCT("1", "製品に関するお問い合わせ")
    , RECRUIT("2", "人事・採用情報")
    , OTHER("3", "その他");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input03.html の「お問い合わせの種類1」の部分を以下のように変更します。

            <!-- お問い合わせの種類1 -->
            <div class="form-group" id="form-group-type1">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの種類1</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <select name="type1" id="type1" class="form-control" style="width: 250px;" autofocus>
                    <th:block th:each="type1Value,iterStat : ${@vh.values('Type1Values')}">
                      <option value="" th:if="${iterStat.first}">選択してください</option>
                      <option th:value="${type1Value.value}" th:text="${type1Value.text}"></option>
                    </th:block>
                  </select>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>

次は「お問い合わせの種類2」です。src/main/java/ksbysample/webapp/bootnpmgeb/values の下に Type2Values.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.values;

import lombok.AllArgsConstructor;
import lombok.Getter;

@SuppressWarnings("MissingOverride")
@Getter
@AllArgsConstructor
public enum Type2Values implements Values {

    ESTIMATE("1", "見積が欲しい")
    , CATALOGUE("2", "資料が欲しい")
    , OTHER("3", "その他の問い合わせ");

    private final String value;
    private final String text;

}

src/main/resources/templates/web/inquiry/input03.html の「お問い合わせの種類2」の部分を以下のように変更します。

            <!-- お問い合わせの種類2 -->
            <div class="form-group" id="form-group-type2">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの種類2</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <div class="checkbox">
                    <th:block th:each="type2Value : ${@vh.values('Type2Values')}">
                      <label>
                        <input type="checkbox" name="type2" th:value="${type2Value.value}">
                        <th:block th:text="${type2Value.text}">見積が欲しい</th:block>
                      </label>
                    </th:block>
                  </div>
                </div></div>
                <div class="row hidden js-errmsg"><div class="col-sm-10"><p class="form-control-static text-danger"><small>ここにエラーメッセージを表示します</small></p></div></div>
              </div>
            </div>

画面を表示すると「性別」「職業」どちらも問題なく表示されています。

f:id:ksby:20180417012450p:plain

履歴

2018/04/17
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その47 )( Node.js を 8.9.1 → 8.9.4 へ、npm を 5.5.1 → 5.6.0 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その46 )( Spring Boot を 1.5.9 → 1.5.10 へ、error-prone を 2.1.3 → 2.2.0 へ、Geb を 2.0 → 2.1 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Javascript 側のライブラリ/モジュールをバージョンアップします。Node.js や npm も最新版にバージョンアップします。

参照したサイト・書籍

目次

  1. Node.js を 8.9.1 → 8.9.4 へ、npm を 5.5.1 → 5.6.0 へバージョンアップする
  2. モジュールを最新版にバージョンアップする
  3. eslint を 4.10.0 → 4.18.1 へバージョンアップする
  4. windows-build-tools を 1.3.2 → 2.2.1 へ、jest を 21.2.1 → 22.4.2 へバージョンアップする

手順

Node.js を 8.9.1 → 8.9.4 へ、npm を 5.5.1 → 5.6.0 へバージョンアップする

nodist dist コマンドを実行して 8.9.4 がインストール可能か確認します。

f:id:ksby:20180227064943p:plain f:id:ksby:20180227065102p:plain

8.9.4 が表示されていますので、8.9.4 へバージョンアップします。

f:id:ksby:20180227065345p:plain

npm も最新版にバージョンアップします。npm の最新バージョンは https://docs.npmjs.com/ の一番下を見ると 5.6.0 とありましたので、

f:id:ksby:20180227065542p:plain

5.6.0 にバージョンアップします。

f:id:ksby:20180227065932p:plain

モジュールを最新版にバージョンアップする

バージョンアップ可能なモジュールを確認します。IntelliJ IDEA のメインメニューから「File」-「Settings...」を選択して「Settings」ダイアログを開き、画面左側で「Language & Frameworks」-「Node.js and NPM」を選択します。

画面右側にモジュールの一覧と現行バージョン、最新バージョン一覧が表示されます。

f:id:ksby:20180227070746p:plain

今回モジュール一覧には以下のように表示されました。

f:id:ksby:20180227070846p:plain f:id:ksby:20180227070941p:plain

今回は以下の方針でバージョンアップします。

  • bootstrap は現行のまま。4.0.0 にはバージョンアップしない。
  • webpack は現行のまま。4.0.1 にはバージョンアップしない。
  • バージョンアップは npm update ではなく npm install --save-dev autoprefixer@8.0.0(package.json の dependencies に記載されているものは --save、devDependencies に記載されているものは --save-dev にする) のようにバージョンを指定しながらバージョンアップする。
  • 1つずつ上からバージョンアップする。関連しそうなところを動作確認しながら進める。
  • eslint、jest + widows-build-tools は別に章を分けてバージョンアップする。

特に問題がでなければ画面キャプチャは撮りません。

  • npm install --save-dev autoprefixer@8.0.0
  • npm install --save axios@0.18.0
  • npm install --save-dev browser-sync@2.23.6
  • npm install --save jquery@3.3.1
  • npm install --save-dev jquery-mockjax@2.3.0
  • npm install --save mobx@3.5.1
  • npm install --save-dev nock@9.2.2
  • npm install --save-dev npm-run-all@4.1.2
  • postcss-cli は 5.0.0 にバージョンアップすると画面の表示が崩れたので 4.1.1 のままにします。
  • npm install --save-dev stylelint@9.1.1
  • npm install --save-dev stylelint-config-standard@18.1.0
  • npm install --save-dev uglifyjs-webpack-plugin@1.2.2
  • xhr-mock は 2.2.0 にバージョンアップすると npm run test でエラーが出たので 1.9.1 のままにします。

eslint を 4.10.0 → 4.18.1 へバージョンアップする

以下のコマンドを実行します。

  • npm install --save-dev eslint@4.18.1
  • npm install --save-dev eslint-loader@2.0.0
  • npm install --save-dev eslint-plugin-import@2.9.0

npm run build コマンドを実行してみると、特にエラーは出ずに終了しました。

f:id:ksby:20180228012028p:plain

windows-build-tools を 1.3.2 → 2.2.1 へ、jest を 21.2.1 → 22.4.2 へバージョンアップする

コマンドプロンプトを「管理者として実行...」で起動した後、npm install --global --production windows-build-tools@2.2.1 を実行します。

f:id:ksby:20180228012827p:plain

次に普通に起動したコマンドプロンプトnpm install --save-dev jest@22.4.2 を実行します。

f:id:ksby:20180228013351p:plain

npm install --save-dev jest-html-reporter@2.0.0 も実行します。

f:id:ksby:20180228013619p:plain

以前からバージョンアップ後に失敗していた Form.test.js のテストを実行すると、今度は成功しました。

f:id:ksby:20180228013951p:plain

Jest 用のテストを全て実行してみます。IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、以下の画像のように設定します。

f:id:ksby:20180228014421p:plain

テストを実行してみると1つだけ失敗しました。

f:id:ksby:20180228014932p:plain

f:id:ksby:20180228015111p:plain

失敗したテストは以下のコードです。

f:id:ksby:20180228020111p:plain

調査しましたが、原因が全く分かりませんでした。。。 以下の点は分かったのですが、そこからどうすれば良いのかが分かりません。先に進みたいのでコメントアウトすることにします。

  • $.ajaxdataType: "jsonp" を指定していると 郵便番号検索API を呼び出してから戻ってこない。
  • dataType: "jsonp" を削除すると Error: Cross origin null forbidden というエラーメッセージが表示される。

再度 IntelliJ IDEA から Jest 用のテストを全て実行すると今度は成功しました。

f:id:ksby:20180305004037p:plain

jest-html-reporter のレポートも問題なく表示されていました。npm run test を実行してレポートを生成すると、以下のように表示されました(エラーが出た時と正常時の時のキャプチャを載せておきます)。

f:id:ksby:20180305004649p:plain f:id:ksby:20180305004916p:plain

履歴

2018/03/05
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その46 )( Spring Boot を 1.5.9 → 1.5.10 へ、error-prone を 2.1.3 → 2.2.0 へ、Geb を 2.0 → 2.1 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その45 )( gradle の build タスク実行時に Javascript の build+テスト を実行する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3以降の作成を進める前にライブラリ/モジュールを一通りバージョンアップすることにします。
    • 2回に分けて、今回は Java 側、次回は Javascript 側のライブラリ/モジュールをバージョンアップします。

参照したサイト・書籍

目次

  1. Spring Boot を 1.5.9 → 1.5.10 へバージョンアップする(他のライブラリもバージョンアップする)
  2. error-prone を 2.1.3 → 2.2.0 へバージョンアップする
  3. Geb を 2.0 → 2.1 へバージョンアップする

手順

Spring Boot を 1.5.9 → 1.5.10 へバージョンアップする(他のライブラリもバージョンアップする)

build.gradle の以下の点を変更します。

group 'ksbysample'
version '1.0.0-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.10.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.13")
        classpath("com.moowork.gradle:gradle-node-plugin:1.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'com.moowork.node'

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '3.5'
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += [
        '-Xep:RemoveUnusedImports:WARN'
        , '-Xep:NestedInstanceOfConditions:OFF'
        , '-Xep:InstanceOfAndCastMatchWrongType:OFF'
        , '-Xep:ParameterName:OFF'
]

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

configurations {
    // for Doma 2
    domaGenRuntime
}

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = '8.8'
    sourceSets = [project.sourceSets.main]
}

findbugs {
    toolVersion = '3.0.1'
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.sql'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

pmd {
    toolVersion = "6.1.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}

//task npmTest(type: NpmTask) {
//    args = ["test"]
//}
task npmTest(type: Exec) {
    commandLine "cmd", "/c", "npm test >NUL 2>&1"
}
task npmBuild(type: Exec) {
    commandLine "cmd", "/c", "npm run build >NUL 2>&1"
}
npmBuild.dependsOn npmTest
processResources.dependsOn npmBuild

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        // BOM は https://repo.spring.io/release/io/spring/platform/platform-bom/Brussels-SR6/
        // の下を見ること
        mavenBom("io.spring.platform:platform-bom:Brussels-SR7") {
            bomProperty 'guava.version', '22.0'
            bomProperty 'thymeleaf.version', '3.0.9.RELEASE'
            bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
            bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
            bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
            bomProperty 'thymeleaf-extras-java8time.version', '3.0.1.RELEASE'
        }
    }
}

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
}

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.1"
    def lombokVersion = "1.16.20"
    def errorproneVersion = "2.1.3"
    def powermockVersion = "1.7.3"
    def seleniumVersion = "3.6.0"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("com.google.guava:guava")
    compile("org.apache.commons:commons-lang3")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test")
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
    compile("org.flywaydb:flyway-core:5.0.7")
    compile("com.h2database:h2:1.4.192")
    compile("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
    testCompile("org.dbunit:dbunit:2.5.4")
    testCompile("com.icegreen:greenmail:1.5.7")
    testCompile("org.assertj:assertj-core:3.9.1")
    testCompile("org.spockframework:spock-core:${spockVersion}")
    testCompile("org.spockframework:spock-spring:${spockVersion}")
    testCompile("com.google.code.findbugs:jsr305:3.0.2")
    testCompile("org.jsoup:jsoup:1.11.2")

    // for lombok
    compileOnly("org.projectlombok:lombok:${lombokVersion}")
    testCompileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    compile("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("com.h2database:h2:1.4.192")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")

    // PowerMock
    testCompile("org.powermock:powermock-module-junit4:${powermockVersion}")
    testCompile("org.powermock:powermock-api-mockito:${powermockVersion}")

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0")
    testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}

..........
  • buildscript の以下の点を変更します。
    • springBootVersion = '1.5.9.RELEASE'springBootVersion = '1.5.10.RELEASE' に変更します。
  • checkstyle の以下の点を変更します。
    • toolVersion = '8.5'toolVersion = '8.8' に変更します。
  • pmd の以下の点を変更します。
    • toolVersion = "5.8.1"toolVersion = "6.1.0" に変更します。
  • dependencyManagement の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Brussels-SR6")mavenBom("io.spring.platform:platform-bom:Brussels-SR7") に変更します。
  • dependencies の以下の点を変更します。
    • def domaVersion = "2.16.1"def domaVersion = "2.19.1" に変更します。
    • def lombokVersion = "1.16.18"def lombokVersion = "1.16.20" に変更します。
    • compile("org.flywaydb:flyway-core:4.2.0")compile("org.flywaydb:flyway-core:5.0.7") に変更します。
    • testCompile("com.icegreen:greenmail:1.5.5")testCompile("com.icegreen:greenmail:1.5.7") に変更します。
    • testCompile("org.assertj:assertj-core:3.8.0")testCompile("org.assertj:assertj-core:3.9.1") に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみますが、pmd がなんか大量のメッセージを出力します。。。

f:id:ksby:20180227012356p:plain

メジャーバージョンアップだけあって、結構大きな変更が行われているようです。今回 pmd は toolVersion = "5.8.1" に戻すことにします。

再度 clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20180227012920p:plain

Project Tool Window で src/test/groovy/ksbysample を選択した後、コンテキストメニューを表示して「Run 'Tests in 'ksbysample'' with Coverage」を選択し、テストが全て成功することも確認できました。

f:id:ksby:20180227014007p:plain

error-prone を 2.1.3 → 2.2.0 へバージョンアップする

gradle-errorprone-plugin を見ると最新バージョンは 0.0.13、mvnrepository.com の error_prone_core のページ を見ると最新バージョンは 2.2.0 でした。

build.gradle の以下の点を変更します。

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.1"
    def lombokVersion = "1.16.20"
    def errorproneVersion = "2.2.0"
    def powermockVersion = "1.7.3"
    def seleniumVersion = "3.6.0"

    ..........
  • dependencies 内で def errorproneVersion = "2.1.3"def errorproneVersion = "2.2.0" に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると今回はエラーは出ずに "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20180227015632p:plain f:id:ksby:20180227015750p:plain

Geb を 2.0 → 2.1 へバージョンアップする

http://www.gebish.org/ を見ると 2.1 がリリースされていたので、バージョンアップします。

build.gradle の以下の点を変更します。

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.1"
    def lombokVersion = "1.16.20"
    def errorproneVersion = "2.2.0"
    def powermockVersion = "1.7.3"
    def seleniumVersion = "3.9.1"

    ..........

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.1")
    testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • dependencies の以下の点を変更します。
    • def seleniumVersion = "3.6.0"def seleniumVersion = "3.9.1" に変更します。
    • testCompile("org.gebish:geb-spock:2.0")testCompile("org.gebish:geb-spock:2.1") に変更します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されました(キャプチャは省略)。

Tomcat を起動した後 gebTest タスクを実行すると、こちらも "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20180227021347p:plain

Tomcat を起動したまま、Project Tool Window で src/test/groovy/geb を選択した後、コンテキストメニューを表示して「Run 'Tests in 'geb''」を選択し、テストが全て成功することも確認できました。

f:id:ksby:20180227021729p:plain

履歴

2018/02/27
初版発行。

Java SE を 8u152 → 8u162 へ、IntelliJ IDEA を 2017.3.2 → 2017.3.4 へ、Git for Windows を 2.15.1(2) → 2.16.2 へバージョンアップ

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

Java SE を 8u152 → 8u162 へバージョンアップする

  1. OracleJava SE Downloads を見ると 8u162 がダウンロードできるようになっていました。以下のページに説明があります。

    8u162 へバージョンアップします。

  2. jdk-8u162-windows-x64.exe をダウンロードして C:\Java\jdk1.8.0_162 へインストールした後、環境変数 JAVA_HOME のパスを C:\Java\jdk1.8.0_162 へ変更します。

    コマンドプロンプトから java -version を実行し、1.8.0_162 に変更されていることを確認します。

    f:id:ksby:20180224183441p:plain

  3. IntelliJ IDEA を再起動した後、プロジェクトで使用する Java SE を 8u162 へ変更します。

  4. 開いているプロジェクトを閉じて「Welcome to IntelliJ IDEA」ダイアログを表示します。

  5. ダイアログ下部の「Configure」-「Project Defaults」-「Project Structure」を選択します。

    f:id:ksby:20180224183727p:plain

  6. 「Default Project Structure」ダイアログが表示されます。画面左側で「Project Settings」-「Project」を選択後、画面右側の「Project SDK」の「New...」ボタンをクリックし、表示されるメニューから「JDK」を選択します。

    f:id:ksby:20180224184215p:plain

  7. 「Select Home Directory for JDK」ダイアログが表示されます。C:\Java\jdk1.8.0_162 を選択した後、「OK」ボタンをクリックします。

    f:id:ksby:20180224184345p:plain

  8. 「Default Project Structure」ダイアログに戻るので、今度は「Project SDK」の「Edit」ボタンをクリックします。

    f:id:ksby:20180224184513p:plain

  9. 画面左側で「Platform Settings」-「SDKs」が選択された状態になるので、画面右上の入力フィールドで "1.8" → "1.8.0_162" へ変更します。

    f:id:ksby:20180224184710p:plain

  10. 次に中央のリストから「1.8.0_152」を選択した後、リストの上の「-」ボタンをクリックして削除します。

    f:id:ksby:20180224184839p:plain

  11. 「OK」ボタンをクリックして「Default Project Structure」ダイアログを閉じます。

  12. 「Welcome to IntelliJ IDEA」ダイアログに戻ったら、ksbysample-webapp-lending プロジェクトを開きます。

  13. IntelliJ IDEA のメイン画面が開いたら、メニューから「File」-「Project Structure...」を選択します。

  14. 「Project Structure」ダイアログが表示されます。以下の画像の状態になっているので、

    f:id:ksby:20180224185120p:plain

    「Project SDK」と「Project language level」を選択し直します。

    f:id:ksby:20180224185246p:plain

  15. 「OK」ボタンをクリックして「Project Structure」ダイアログを閉じます。

  16. メイン画面に戻ると画面右下に「Indexing...」の表示が出るので、終了するまで待ちます。

    f:id:ksby:20180224185403p:plain

  17. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20180224185944p:plain

  18. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run ‘All Tests’ with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20180224190433p:plain

  19. 特に問題は発生しませんでした。8u162 で開発を進めます。

IntelliJ IDEA を 2017.3.2 → 2017.3.4 へバージョンアップする

IntelliJ IDEA の 2017.3.4 がリリースされているのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  2. IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。

    f:id:ksby:20180224190954p:plain

  3. Plugin の update も表示されました。このまま「Update and Restart」ボタンをクリックします。

    f:id:ksby:20180224191046p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。再起動時に「Data Sharing Options」ダイアログが表示されるので、「Send anonymous usage statistics to JetBrains」をチェックして「OK」ボタンをクリックします。

    f:id:ksby:20180224191638p:plain

  5. IntelliJ IDEA が起動しましたが、今回は画面下部に「Indexing…」のメッセージは表示されませんでした。

  6. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.3.4 へバージョンアップされていることを確認します。

  7. Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20180224205440p:plain

  8. clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。

    f:id:ksby:20180224210033p:plain

  9. Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。

    f:id:ksby:20180224210516p:plain

Git for Windows を 2.15.1(2) → 2.16.2 へバージョンアップする

Git for Windows の 2.16.2 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.16.2-64-bit.exe をダウンロードします。

  2. 「Git 2.16.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  3. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。

  4. 「Choosing the default editor used by Git」画面が表示されます。「Use Vim (the ubiquitous text editor) as Git's default editor」が選択された状態で [Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git --version を実行し、git のバージョンが git version 2.16.2.windows.1 になっていることを確認します。

    f:id:ksby:20180224211555p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20180224211725p:plain

  13. 特に問題はないようですので、2.16.2 で作業を進めたいと思います。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その45 )( gradle の build タスク実行時に Javascript の build+テスト を実行する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Flow を試してみようと思っていましたが、途中からちょっと入れて試すようなものではなさそうだったので、止めました。
    • Gradle の build タスクを実行した時に、Jest のテストと webpack による build を一緒に実行できるようにする(エラーが出た場合には build タスクを中断する)方法を調べます。

参照したサイト・書籍

  1. gradle-node-plugin 使って Sass をコンパイルしたりすればいいじゃない
    https://qiita.com/macoshita/items/82aa006c75cdd2222abf

  2. srs/gradle-node-plugin
    https://github.com/srs/gradle-node-plugin

  3. jest-html-reporter
    https://www.npmjs.com/package/jest-html-reporter

  4. How can I set NODE_ENV=production on Windows?
    https://stackoverflow.com/questions/9249830/how-can-i-set-node-env-production-on-windows

  5. cross-env
    https://www.npmjs.com/package/cross-env

  6. webpack-contrib/uglifyjs-webpack-plugin
    https://github.com/webpack-contrib/uglifyjs-webpack-plugin

  7. GoogleChromeLabs/webpack-training-project
    https://github.com/GoogleChromeLabs/webpack-training-project

  8. GoogleChromeLabs/webpack-libs-optimizations
    https://github.com/GoogleChromeLabs/webpack-libs-optimizations

目次

  1. 目標
  2. gradle から npm scripts を実行するのに gradle-node-plugin を使用する
  3. gradle から npm scripts を実行するのに task ...(type: Exec) {...} を使用する
  4. jest-html-reporter をインストールする
  5. gradle-node-plugin にするか task ...(type: Exec) {...} にするか?
  6. package.json に build 用の npm scripts を追加する
  7. build.gradle に npmBuild タスクを定義する
  8. npm run build を実行する時は webpack でソースマップを出力せず、uglify する
    1. Windows で NODE_ENV 環境変数で webpack の設定を切り替えられるようにするために cross-env モジュールをインストールする
    2. uglifyjs-webpack-plugin をインストールする
    3. webpack.config.js を変更する
    4. package.json を変更する
    5. 動作確認
  9. 次回は。。。

手順

目標

以下のことができるようにしてみたいと思います。

  • build.gradle に npmTest, npmBuild というタスクを作成する。
  • npmTest は npm test、npmBuild は npm run build(新規作成する)に関連させる。
  • npmTest → npmBuild → processResources → ... の順に実行する。
  • npm run springboot を実行する時は webpack でソースマップを出力し、かつ uglify しない。
  • npm run build を実行する時は webpack でソースマップを出力せず、uglify する。

gradle から npm scripts を実行するのに gradle-node-plugin を使用する

gradle から npm scripts を実行するのに srs/gradle-node-plugin というプラグインがあるとのことなので、このプラグインを使用して npm test を実行してみます。

build.gradle を以下のように変更します。

buildscript {
    ext {
        springBootVersion = '1.5.9.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.13")
        classpath("com.moowork.gradle:gradle-node-plugin:1.2.0")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'
apply plugin: 'com.moowork.node'

..........

task npmTest(type: NpmTask) {
    args = ["test"]
}
processResources.dependsOn npmTest

..........
  • buildscript に classpath("com.moowork.gradle:gradle-node-plugin:1.2.0") を追加します。
  • apply plugin: 'com.moowork.node' を追加します。
  • task npmTest(type: NpmTask) { ... } を追加します。
  • processResources.dependsOn npmTest を追加します。

clean タスク → build タスクの順に実行してみます。現在 Jest のテストは全て成功する状態です。

f:id:ksby:20180113012537p:plain .....(長いので途中は省略します)..... f:id:ksby:20180113012951p:plain

npmTest タスクは正常終了し、"BUILD SUCCESSFUL" も出力されました。ただし、

  • テストファイル1つに付き、PASS の文字が付いた行が出力されます。出来れば Java のテストのように成功時は何も出力されないようにしたいです。
  • Jest のテストに console.log(....); を書いているものがあるのですが、コンソールにログが大量に出力されました。gradle-node-plugin や Gradle の Webサイトを見てみたのですが、ログを抑制する方法が分かりません。。。

gradle から npm scripts を実行するのに task ...(type: Exec) {...} を使用する

cmd.exe で npm コマンドを実行して標準出力/エラー出力を NUL へ送ればコンソールにログが出力されないはずなので、task ...(type: Exec) {...} でタスクを定義してみます。

build.gradle を以下のように変更します。

//task npmTest(type: NpmTask) {
//    args = ["test"]
//}
task npmTest(type: Exec) {
    commandLine "cmd", "/c", "npm test >NUL 2>&1"
}
processResources.dependsOn npmTest
  • task npmTest(type: NpmTask) { ... }コメントアウトします。
  • task npmTest(type: Exec) { ... } を追加します。>NUL 2>&1 を付けて npm test の標準出力、エラー出力を一切出ないようにします。

clean タスク → build タスクの順に実行してみます。

f:id:ksby:20180119010456p:plain

npmTest タスクは正常終了し、"BUILD SUCCESSFUL" も出力されました。余計なログも出力されません。個人的にはこの出力結果の方が理想です。

テストが途中で失敗した時にそこで止まって先のタスクに進まないのかが気になるので試してみます。

src/test/assets/tests/lib/util/ZipcloudApiHelper.test.js の以下の点を変更します。

    // zipcloudApiHeler.search の dataType: "jsonp" を削除しないと下のテストは成功しないので
    // コメントアウトしておく
    describe("jquery-mockjax でサーバ側をモックにして $.ajax をテストする", () => {

        afterEach(() => {
            mockjax.clear();
        });
  • describe("jquery-mockjax でサーバ側をモックにして $.ajax をテストする", () => { ... } の前後の /**/ を削除し、コメントアウトを解除します。これで Jest のテストで失敗するものが出ます。

clean タスク → build タスクの順に実行してみると、npmTest タスクでエラーが出て止まりました。

f:id:ksby:20180120004638p:plain

ただしエラーがどこで出たのかレポートファイルで確認しようと思ったのですが、coverage のレポートしか出ていませんでした。Jest のページを見ましたが、テスト結果のレポートを出力させる方法がよく分かりません。。。

f:id:ksby:20180120010349p:plain

jest-html-reporter をインストールして試してみる

レポートファイルを生成する方法を調べてみたところ、jest-html-reporter というモジュールを見つけました。インストールして試してみます。

npm install --save-dev jest-html-reporter コマンドを実行してインストールします。

f:id:ksby:20180120012142p:plain

jest.config.json を以下のように変更します。

{
  "coverageDirectory": "build/reports/jest",
  "moduleDirectories": [
    "node_modules",
    "src/main/assets/js"
  ],
  "testResultsProcessor": "./node_modules/jest-html-reporter"
}
  • "testResultsProcessor": "./node_modules/jest-html-reporter" を追加します。

jest-html-reporter の設定は package.json に記述するようなので、package.json を以下のように変更します。

  "license": "ISC",
  "jest-html-reporter": {
    "outputPath": "build/reports/jest/jest-html-reporter.html",
    "includeFailureMsg": true
  },
  "dependencies": {
  • "jest-html-reporter": { ... } を追加します。

clean タスク → build タスクの順に実行してみます。先程と同様のエラーが出た後、build/reports/jest の下に jest-html-reporter.html が生成されていました。

f:id:ksby:20180120075554p:plain

jest-html-reporter.html をブラウザで表示させると、失敗したテストとエラーメッセージが表示されていました。

f:id:ksby:20180120075708p:plain

必要な情報が出ていて申し分がないので、jest-html-reporter をこのまま使用することにします。

gradle-node-plugin にするか task ...(type: Exec) {...} にするか?

環境依存しないと思われる gradle-node-plugin の方が良いとは思うのですが、task ...(type: Exec) {...} の方が不要な出力が出ず動作が好みなので、今回はこちらの方法を使用することにします。

package.json に build 用の npm scripts を追加する

package.jsonnpm run build で実行する npm scripts を定義します。

  "scripts": {
    "test": "jest --config=jest.config.json --coverage",
    "postinstall": "run-s clean:static-dir copy:all",
    "clean:static-dir": "rimraf src/main/resources/static/*",
    "clean:cssjs-dir": "rimraf src/main/resources/static/{css,js}/*",
    "copy:all": "run-p copy:bootstrap copy:admin-lte copy:font-awesome copy:ionicons copy:jquery-ui-css",
    "copy:bootstrap": "cpx node_modules/bootstrap/dist/**/* src/main/resources/static/vendor/bootstrap",
    "copy:admin-lte": "cpx node_modules/admin-lte/dist/**/* src/main/resources/static/vendor/admin-lte",
    "copy:font-awesome": "cpx node_modules/font-awesome/{css,fonts}/**/* src/main/resources/static/vendor/font-awesome",
    "copy:ionicons": "cpx node_modules/ionicons/dist/{css,fonts}/**/* src/main/resources/static/vendor/ionicons",
    "copy:jquery-ui-css": "cpx node_modules/jquery-ui/themes/base/**/* src/main/resources/static/vendor/jquery-ui/css",
    "postcss:build": "postcss src/main/assets/css/**/* -d src/main/resources/static/css -x .min.css",
    "postcss:watch": "postcss src/main/assets/css/**/* -d src/main/resources/static/css -x .min.css -w --poll",
    "webpack:build": "webpack",
    "webpack:watch": "webpack --watch",
    "browser-sync:start": "browser-sync start --config bs-config.js",
    "browser-sync:springboot": "browser-sync start --config bs-springboot-config.js",
    "server": "run-p postcss:watch webpack:watch browser-sync:start",
    "springboot": "run-p postcss:watch webpack:watch browser-sync:springboot",
    "build": "run-s clean:cssjs-dir postcss:build webpack:build"
  },
  • 以下の4行を追加します。
    • "clean:cssjs-dir": "rimraf src/main/resources/static/{css,js}/*"
    • "postcss:build": "postcss src/main/assets/css/**/* -d src/main/resources/static/css -x .min.css"
    • "webpack:build": "webpack"
    • "build": "run-s clean:cssjs-dir postcss:build webpack:build"

コマンドプロンプトから npm run build を実行して正常に動作することを確認します。

f:id:ksby:20180220004635p:plain

build.gradle に npmBuild タスクを定義する

build.gradle を以下のように変更します。

..........

//task npmTest(type: NpmTask) {
//    args = ["test"]
//}
task npmTest(type: Exec) {
    commandLine "cmd", "/c", "npm test >NUL 2>&1"
}
task npmBuild(type: Exec) {
    commandLine "cmd", "/c", "npm run build >NUL 2>&1"
}
npmBuild.dependsOn npmTest
processResources.dependsOn npmBuild

repositories {
    mavenCentral()
}

..........
  • task npmBuild(type: Exec) { ... } を追加します。
  • npmBuild.dependsOn npmTestprocessResources.dependsOn npmBuild を追加します。

これで npmTest → npmBuild → processResources → ... の順にタスクが実行されるはずです。clean タスク → build タスクの順に実行してみます。

f:id:ksby:20180220005756p:plain

全てのタスクが正常終了し、"BUILD SUCCESSFUL" が出力されました。問題なさそうです。

npm run build を実行する時は webpack でソースマップを出力せず、uglify する

Windows で NODE_ENV 環境変数で webpack の設定を切り替えられるようにするために cross-env モジュールをインストールする

Spring Boot の spring.profiles.active のように webpack でも環境変数で設定を切り替えたい場合 NODE_ENV を使用しますが、npm scripts を "build": "NODE_ENV=product run-s clean:cssjs-dir postcss:build webpack:build" のように NODE_ENV=product を付けても Windows 環境では以下の画像のようにエラーになります。

f:id:ksby:20180221003011p:plain

エラーにならない方法を調べたところ、How can I set NODE_ENV=production on Windows?cross-env モジュールを使う方法が書かれていました。

npm install --save-dev cross-env コマンドを実行してインストールします。

f:id:ksby:20180221003701p:plain

uglifyjs-webpack-plugin をインストールする

webpack で js ファイルを uglify するには uglifyjs-webpack-plugin を使えばいいようです。npm install --save-dev uglifyjs-webpack-plugin コマンドを実行してインストールします。

f:id:ksby:20180221004220p:plain

webpack.config.js を変更する

GoogleChromeLabs/webpack-training-project を参考に、NODE_ENV=product が設定されている時だけ uglifyjs-webpack-plugin が適用されるようにします。ソースマップは uglifyjs-webpack-plugin が適用されると webpack.config.js に devtool: "inline-source-map" が記述されていても出力されなくなります。

const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const isProduct = process.env.NODE_ENV === "product";

module.exports = {
    ..........
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        })
    ].concat(
        isProduct
            ? [
                new UglifyJsPlugin()
            ]
            : []
    ),
    devtool: "inline-source-map"
};
  • 以下の2行を追加します。
    • const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
    • const isProduct = process.env.NODE_ENV === "product";
  • plugins に .concat(isProduct ? [new UglifyJsPlugin()] : []) を追加します。

package.json を変更する

package.json を以下のように変更します。

  "scripts": {
    ..........
    "springboot": "run-p postcss:watch webpack:watch browser-sync:springboot",
    "build": "cross-env NODE_ENV=product run-s clean:cssjs-dir postcss:build webpack:build"
  },
  • npm scripts の build の先頭に cross-env NODE_ENV=product を追加します。

動作確認

コマンドプロンプトから npm run build コマンドを実行してみます。

f:id:ksby:20180221005823p:plain

エラーが出ずに終了し、webpack でバンドルした後の js ファイルが 100KB 前後とかなり小さくなりました(上にある画像キャプチャを見ると cross-env NODE_ENV=product を指定していない npm run build コマンドでは 750KB ~ 1MB 程度でした)。ただし uglify するためか処理時間が 18秒もかかりますね。

ちなみに webpack.config.js から devtool: "inline-source-map" を削除して npm run build コマンドを実行しても生成される js ファイルのサイズは変わりません。

f:id:ksby:20180221065908p:plain

clean タスク → build タスクを実行すると、こちらも問題なく終了しました。

f:id:ksby:20180221011627p:plain

webpack によりバンドルされた input01.js を開いてみると、確かに uglify されています。

f:id:ksby:20180221014005p:plain

次回は。。。

入力画面3以降の作成を進めます。

履歴

2018/02/21
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる2 )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( MobX を使用してみる ) の続きです。

参照したサイト・書籍

  1. danielearwicker / computed-async-mobx
    https://github.com/danielearwicker/computed-async-mobx

目次

  1. autorun を複数定義するとどのように動作するのか?
  2. computed-async-mobx を使用して API を呼び出す非同期処理をクラス内に記述する
  3. action で API を呼び出す非同期処理をクラス内に記述する
  4. reaction で API を呼び出す非同期処理をクラス内に記述する
  5. reaction で複数のプロパティを監視するには [] で囲む

手順

autorun を複数定義するとどのように動作するのか?

mobx.autorun(...) を複数定義すると最後に定義されたものが最初に実行されるようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("autorun を複数定義すると最後に定義したもの→最初に定義したものの順に実行される", () => {
        document.body.innerHTML = `
            <div id="value"></div>
            <div id="data"></div>
            <div id="all"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    data: "",
                    all: mobx.computed(() => `${this.value}, ${this.data}`)
                });
            }
        }

        const sample = new Sample();
        mobx.autorun(() => {
            console.log("PASS1");
            $("#value").text(sample.value);
        });
        mobx.autorun(() => {
            console.log("PASS2");
            $("#data").text(sample.data);
        });
        mobx.autorun(() => {
            console.log("PASS3");
            $("#all").text(sample.all);
        });

        expect($("#value").text()).toBe("");
        expect($("#data").text()).toBe("");
        expect($("#all").text()).toBe(", ");

        console.log("★★★");
        sample.value = "1";
        expect($("#value").text()).toBe("1");
        expect($("#data").text()).toBe("");
        expect($("#all").text()).toBe("1, ");

        console.log("●●●");
        sample.data = "a";
        expect($("#value").text()).toBe("1");
        expect($("#data").text()).toBe("a");
        expect($("#all").text()).toBe("1, a");
    });

テストを実行すると PASS3 → PASS1、PASS3 → PASS2 と最後に定義されたものから順に実行されることが確認できます。

f:id:ksby:20180106204147p:plain

computed-async-mobx を使用して API を呼び出す非同期処理をクラス内に記述する

プロパティの値が更新されたら外部の API を呼び出して別のプロパティの値を更新する処理を mobx.autorunAsync(...) で定義してもいいのですが、mobx.computed(...) のようにクラス内で定義できると便利かなと思って調べたところ、danielearwicker/computed-async-mobx というモジュールを見つけましたので試してみます。

npm install --save computed-async-mobx コマンドを実行してインストールします。

f:id:ksby:20180106205528p:plain

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加します。

    test("computed-async-mobx を使用するとクラス内に API 呼び出しの非同期処理を定義できる", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // computed-async-mobx.asyncComputed(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: computedAsyncMobx.asyncComputed("", 0, async () => {
                        console.log("PASS1-1");
                        if (this.name === "") {
                            return "";
                        }
                        console.log("PASS1-2");
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        return json.weather[0].main;
                    })
                });
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            // area.weather ではなく area.weather.get() で値を取得する点に注意する
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather.get());
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、以下のことが分かります。

  • computedAsyncMobx.asyncComputed(...) は初回定義時に1度実行される(console.log("(1) ..."); の前に "PASS1-1" がログに出力されている)。
  • $("#name").val() を更新すると (1)~(5) の順で実行される。

f:id:ksby:20180106223021p:plain

action で API を呼び出す非同期処理をクラス内に記述する

MobX のマニュアルを読んでいたら Writing asynchronous actions のページを見つけました。mobx.action(...) を使用すれば API を呼び出す非同期処理をクラスのメソッドとして実装できます。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.action を使用して API 呼び出しの非同期処理を記述する", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // mobx.action(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: "",
                    getWeather: mobx.action(async () => {
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        this.weather = json.weather[0].main;
                    })
                });
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather);
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
            // action は自動実行はされないので、ここで area.getWeather(); を呼び出す
            area.getWeather();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、(1)~(5) の順で処理が進むことが確認できます。

f:id:ksby:20180107070022p:plain

reaction で API を呼び出す非同期処理をクラス内に記述する

mobx.action(...) でもいいのですが、MobX の機能だけでプロパティを更新したら自動的に API を呼び出して天気を取得するように出来ないかマニュアルを見ていたところ、Reaction というページを見つけました。mobx.reaction(...) で監視対象のプロパティと実行する処理を記述すればよいようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.reaction を使用すれば、プロパティ更新時に自動で非同期処理を実行できる", done => {
        // xhr-mock でモックを定義する
        xhrmock.get(/^http:\/\/api\.openweathermap\.org\/data\/2\.5\/weather/
            , (req, res) => {
                return res
                    .status(200)
                    .body({
                        weather: [
                            {
                                id: 500,
                                main: "Rain",
                                description: "light rain",
                                icon: "10d"
                            }
                        ],
                        name: "Tokyo"
                    });
            });

        document.body.innerHTML = `
            <div id="area">
                <input type="text" name="name" id="name" value="">
                <div id="weather"></div>
            </div>
        `;

        // API を呼び出す処理を mobx.autorunAsync(...) ではなく、
        // mobx.reaction(...) を使用してクラス内に定義する
        class Area {
            constructor() {
                mobx.extendObservable(this, {
                    name: "",
                    weather: ""
                });
                mobx.reaction(
                    () => this.name,
                    async () => {
                        console.log("(3) area.name が更新されたら OpenWeatherMap の API で天気を取得し、area.weather にセットする");
                        const res = await openWeatherMapHelper.getCurrentWeatherDataByCityName(this.name);
                        const json = res.data;
                        this.weather = json.weather[0].main;
                    }
                );
            }
        }

        const area = new Area();
        // area.weather が更新されたら $("#weather").text() にセットする
        mobx.autorun(() => {
            console.log("(4) area.weather が更新されたら $(\"#weather\").text() にセットする");
            $("#weather").text(area.weather);
        });

        // $("#name").val() に入力された値を area.name にセットする
        $("#name").on("blur", event => {
            console.log("(2) $(\"#name\").val() に入力された値を area.name にセットする");
            area.name = $(event.target).val();
        });

        console.log("(1) $(\"#name\").val() に Tokyo と入力する");
        $("#name").val("Tokyo").blur();

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 1100);
    });

テストを実行すると、(1)~(5) の順で処理が進むことが確認できます。computedAsyncMobx.asyncComputed(...)` を使わなくてもこれで良さそうです。

f:id:ksby:20180107071154p:plain

またプロパティを再度更新したら API を呼び出す処理が実行されるのか、テストを以下のように修正してcomputedAsyncMobx.asyncComputed(...)mobx.action(...)mobx.reaction(...) の3パターンで試してみましたが、これは全て API を呼び出す処理を実行しました。mobx.reaction(...)mobx.when(...) と同じで1度しか実行されないのでは?と思いましたが、そうではないようです。

        let doneFlg = false;
        setTimeout(() => {
            console.log("(5) $(\"#weather\").text() に Rain がセットされていることを確認する");
            expect($("#weather").text()).toBe("Rain");
            // なぜかここに done(); を書くと console.log("(5) ..."); のログが出力されない
            // done();
            doneFlg = true;
            $("#name").val("Oosaka").blur();
        }, 1000);

        setTimeout(() => {
            if (doneFlg) {
                done();
            }
        }, 2000);
  • doneFlg = true; の後に $("#name").val("Oosaka").blur(); を追加します。
  • done(); を呼び出す時の遅延時間を 1100 → 2000 へ変更します(1100 のままだと動作しない場合があったため)。

reaction で複数のプロパティを監視するには [] で囲む

mobx.reaction(...) で複数プロパティを監視したい場合には配列で指定するとできるようです。

src/test/assets/tests/lib/util/mobx.test.js に以下のテストを追加して確認してみます。

    test("mobx.reaction で複数プロパティを監視するには [] で囲んで指定する", () => {
        document.body.innerHTML = `
            <div id="sample"></div>
        `;

        class Sample {
            constructor() {
                mobx.extendObservable(this, {
                    value: "",
                    data: "",
                    pass: ""
                });
                mobx.reaction(
                    () => [this.value, this.data, this.pass],
                    ([value, data, pass]) => {
                        $("#sample").text(`value = ${value}, data = ${data}, pass = ${pass}`);
                    }
                );
            }
        }

        const sample = new Sample();
        console.log("★★★ " + $("#sample").text());
        sample.value = "a";
        console.log("★★★ " + $("#sample").text());
        sample.data = "1";
        console.log("★★★ " + $("#sample").text());
        sample.pass = "PASS1";
        console.log("★★★ " + $("#sample").text());
        sample.value = "b";
        console.log("★★★ " + $("#sample").text());
        sample.data = "2";
        console.log("★★★ " + $("#sample").text());
        sample.pass = "PASS2";
        console.log("★★★ " + $("#sample").text());
    });

実行すると、mobx.reaction(...) が実行されて反映されることが確認できます。

f:id:ksby:20180109004444p:plain

履歴

2018/01/09
初版発行。