かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その55 )( PMD を 5.8.1 → 6.4.0 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その54 )( webpack を 3.8.1 → 4.9.1 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • PMD を 5.8.1 → 6.4.0 へバージョンアップします。2~3回に分ける予定です。
    • PMD 6.0.0 から Java 9 に対応しており(6.4.0 で Java 10 に対応)、Rule が8種類の Category に分類されて独自の Rulesets の書き方も変わっています。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. 変更の方針を決める
  3. category/java/bestpractices.xml
    1. Unsupported build listener: class org.gradle.api.internal.project.ant.AntLoggingAdapterUnsupported build listener: class org.apache.tools.ant.AntClassLoader
    2. This analysis could be faster, please consider using Incremental Analysis: https://pmd.github.io/pmd-6.4.0/pmd_userdocs_getting_started.html#incremental-analysis
  4. category/java/codestyle.xml
    1. Variables that are final and static should be all capitals, '...' is not all capitals.
    2. Parameter '...' is not assigned and could be declared final
    3. Avoid excessively long variable names like ...
    4. Local variable '...' could be declared final
    5. Each class should declare at least one constructor
    6. Avoid variables with short names like ...
    7. Avoid the use of value in annotations when its the only element
    8. Avoid unnecessary constructors - the compiler will generate these for you
    9. The utility class name '...' doesn't match '[A-Z][a-zA-Z0-9]+(Utils?|Helper)'
    10. Use explicit scoping instead of the default package private level
    11. To avoid mistakes add a comment at the beginning of the ... field if you want a default access modifier
    12. A method should have only one exit point, and that should be the last statement in the method
  5. category/java/design.xml
    1. Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified
    2. All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.
    3. The method '...' has a NCSS line count of ....
    4. Potential violation of Law of Demeter (method chain calls)
    5. This class has too many methods, consider refactoring it.
    6. The class '...' is suspected to be a Data Class
    7. Avoid throwing raw exception types.
    8. Rather than using a lot of String arguments, consider using a container object for those values.
    9. The method '...' has a cyclomatic complexity of ....
  6. ここまでやってみた感想

手順

build.gradle を変更する

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

pmd {
    toolVersion = "6.4.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • toolVersion = "5.8.1"toolVersion = "6.4.0" に変更します。

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

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみますが、相変わらず大量にエラーが出ています。。。

f:id:ksby:20180602142545p:plain

変更の方針を決める

PMD Release Notes - 15-December-2017 - 6.0.0PMD Making RulesetsJava Rules を見ると、Rule の Category が一新されていて1から見直した方が良さそうに思えたので、以下の方針で変更します。

  • pmd-project-rulesets.xml は1から作り直します。build タスク実行で出力されたエラーメッセージを元に修正はしません。
  • pmd-project-rulesets.xml には <rule ref="category/java/bestpractices.xml" /> のフォーマットで Category の xml ファイルを1つずつ記述して、build タスクを実行して結果を見て調整します。
  • 現在 exclude している Rule は、基本的に同じように exclude します。

config/pmd/pmd-project-rulesets.xml を以下の内容に書き換えます。

<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="mybraces"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
    <description>project rulesets</description>

    <!--
        rulesets の種類・説明は 以下の URL 参照
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/category/java
        https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/rulesets/java
        https://pmd.github.io/pmd-6.4.0/pmd_rules_java.html
        ※"pmd-6.4.0" の部分は適用しているバージョンに変更すること。
    -->
    <rule ref="category/java/bestpractices.xml"/>
    ..........
</ruleset>

<rule ref="category/java/bestpractices.xml"/> の部分に1つずつ category を記述して調整していきます。

category/java/bestpractices.xml

PMD のルール違反は出ませんでしたが、それ以外で以下のメッセージが出力されました。

:pmdMain
Unsupported build listener: class org.gradle.api.internal.project.ant.AntLoggingAdapter
Unsupported build listener: class org.apache.tools.ant.AntClassLoader
This analysis could be faster, please consider using Incremental Analysis: https://pmd.github.io/pmd-6.4.0/pmd_userdocs_getting_started.html#incremental-analysis
This analysis could be faster, please consider using Incremental Analysis: https://pmd.github.io/pmd-6.4.0/pmd_userdocs_getting_started.html#incremental-analysis

Unsupported build listener: class org.gradle.api.internal.project.ant.AntLoggingAdapterUnsupported build listener: class org.apache.tools.ant.AntClassLoader

コマンドラインから gradlew cleangradlew --debug pmdMain を実行してみたところ、以下のログが出力されました。

16:06:18.934 [WARN] [org.gradle.api.internal.project.ant.AntLoggingAdapter] Unsupported build listener: class org.gradle.api.internal.project.ant.AntLoggingAdapter
16:06:18.935 [WARN] [org.gradle.api.internal.project.ant.AntLoggingAdapter] Unsupported build listener: class org.apache.tools.ant.AntClassLoader

Unsupported build listener で調べてみると Gradle - Method details - addBuildListener) から Interface BuildListener が見つかりました。AntLoggingAdapter と AntClassLoader のクラスで BuildListener インターフェースが実装されていないのが原因のようです。

WARN のログなので、出力されたままにすることにします。

This analysis could be faster, please consider using Incremental Analysis: https://pmd.github.io/pmd-6.4.0/pmd_userdocs_getting_started.html#incremental-analysis

https://pmd.github.io/pmd-6.4.0/pmd_userdocs_getting_started.html#incremental-analysis のリンク先のページを見ると、なぜか Gradle だけリンクになっていませんね。。。

他にも調べてみると Revert "Support PMD's analysis cache (#2223)" が見つかりました。おそらく原因はこれだと思うのですが、まだ解決していないようです。対策がないようですので、出力されたままにします。

category/java/codestyle.xml

設定を追加して build タスクを実行すると 307 PMD rule violations were found. と出力されました。メッセージは以下の12種類でした。

  • Variables that are final and static should be all capitals, '...' is not all capitals.
  • Parameter '...' is not assigned and could be declared final
  • Avoid excessively long variable names like ...
  • Local variable '...' could be declared final
  • Each class should declare at least one constructor
  • Avoid variables with short names like ...
  • Avoid the use of value in annotations when its the only element
  • Avoid unnecessary constructors - the compiler will generate these for you
  • The utility class name '...' doesn't match '[A-Z][a-zA-Z0-9]+(Utils?|Helper)'
  • Use explicit scoping instead of the default package private level
  • To avoid mistakes add a comment at the beginning of the ... field if you want a default access modifier
  • A method should have only one exit point, and that should be the last statement in the method

Variables that are final and static should be all capitals, '...' is not all capitals.

VariableNamingConventions のルール違反のメッセージです。前も exclude していたので、今回も exclude します。

Parameter '...' is not assigned and could be declared final

MethodArgumentCouldBeFinal のルール違反のメッセージです。前も exclude していたので、今回も exclude します。

Avoid excessively long variable names like ...

LongVariable のルール違反のメッセージです。前も exclude していたので、今回も exclude します。

Local variable '...' could be declared final

LocalVariableCouldBeFinal のルール違反のメッセージです。前も exclude していたので、今回も exclude します。

Each class should declare at least one constructor

AtLeastOneConstructor のルール違反のメッセージです。前は何もしていませんでしたが、今のところ対応する必要性を感じなかったので exclude します。

Avoid variables with short names like ...

ShortVariable のルール違反のメッセージです。前も exclude していたので、今回も exclude します。

Avoid the use of value in annotations when its the only element

UnnecessaryAnnotationValueElement のルール違反のメッセージです。PMD 6.2.0 から新規に追加された Rule です。指摘された箇所のアノテーションは削除しようがなかったので、exclude します。

Avoid unnecessary constructors - the compiler will generate these for you

UnnecessaryConstructor のルール違反のメッセージです。

メッセージが出ているのは src/main/java/ksbysample/webapp/bootnpmgeb/config/DomaConfig.java で、ルール違反を指摘されているコンストラクタを削除しても特に問題なかったので、ソースを修正してメッセージが出ないようにします。

The utility class name '...' doesn't match '[A-Z][a-zA-Z0-9]+(Utils?|Helper)'

ClassNamingConventions のルール違反のメッセージです。

メッセージが出ているのは以下の2つのクラスですが、static のフィールド・メソッドしかないと出ているようです。

  • src/main/java/ksbysample/webapp/bootnpmgeb/constants/UrlConst.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/util/validator/EmailValidator.java

個人的にクラス名を変更したくなかったので、exclude することにします。

Use explicit scoping instead of the default package private level

DefaultPackage のルール違反のメッセージです。

メッセージが出ているのは Doma 2 の Entity クラスで対応しようがなかったので、exclude することにします。

To avoid mistakes add a comment at the beginning of the ... field if you want a default access modifier

CommentDefaultAccessModifier のルール違反のメッセージです。

メッセージが出ているのは Doma 2 の Entity クラスで、DomaGen で生成した時にコメントがついていないのが原因なので(確か COMMENT ON COLUMN ... IS '...'; でカラムにコメントを付けるとコメントに出るはず)、exclude することにします。

A method should have only one exit point, and that should be the last statement in the method

OnlyOneReturn のルール違反のメッセージです。

個人的にはメソッド内に複数 return を入れても構わないと思っているので、exclude します。

以上で category/java/codestyle.xml は以下のような定義になりました。

    <rule ref="category/java/codestyle.xml">
        <exclude name="VariableNamingConventions"/>
        <exclude name="MethodArgumentCouldBeFinal"/>
        <exclude name="LongVariable"/>
        <exclude name="LocalVariableCouldBeFinal"/>
        <exclude name="AtLeastOneConstructor"/>
        <exclude name="ShortVariable"/>
        <exclude name="UnnecessaryAnnotationValueElement"/>
        <exclude name="ClassNamingConventions"/>
        <exclude name="DefaultPackage"/>
        <exclude name="CommentDefaultAccessModifier"/>
        <exclude name="OnlyOneReturn"/>
    </rule>

category/java/design.xml

設定を追加して build タスクを実行すると 95 PMD rule violations were found. と出力されました。メッセージは以下の9種類でした。

  • Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified
  • All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.
  • The method '...' has a NCSS line count of ....
  • Potential violation of Law of Demeter (method chain calls)
  • This class has too many methods, consider refactoring it.
  • The class '...' is suspected to be a Data Class
  • Avoid throwing raw exception types.
  • Rather than using a lot of String arguments, consider using a container object for those values.
  • The method '...' has a cyclomatic complexity of ....

Removed misconfigured rule: LoosePackageCoupling cause: No packages or classes specified

LoosePackageCoupling の設定がないというメッセージのようです。

リンク先の説明を読みましたが、今は必要そうに思えなかったので exclude することにします。

All methods are static. Consider using a utility class instead. Alternatively, you could add a private constructor or make the class abstract to silence this warning.

UseUtilityClass のルール違反のメッセージです。

ClassNamingConventions を exclude したのと同じ理由で、こちらも exclude します。

The method '...' has a NCSS line count of ....

NcssCount のルール違反のメッセージです。PMD 6.0.0 から新規に追加された Rule です。

実はこのルールが何を指摘しているのかよく分かりませんでした。。。 対応方法もよく分からないので exclude します。

Potential violation of Law of Demeter (method chain calls)

LawOfDemeter のルール違反のメッセージです。

リンク先の説明を読みましたが、有効性がよく分からなかったので exclude します。

This class has too many methods, consider refactoring it.

TooManyMethods のルール違反のメッセージです。

このルールは残しておきたいので、メッセージが表示された以下のクラスに @SuppressWarnings("PMD.TooManyMethods") を付けることにします。

  • src/main/java/ksbysample/webapp/bootnpmgeb/aspect/logging/RequestAndResponseLogger.java

The class '...' is suspected to be a Data Class

DataClass のルール違反のメッセージです。PMD 6.0.0 から新規に追加された Rule です。

メッセージが出ていたのが Doma 2 の Entity クラスだったのですが対応のしようがないのと、このルールの必要性も感じなかったので、exclude します。

Avoid throwing raw exception types.

AvoidThrowingRawExceptionTypes のルール違反のメッセージです。

エラーが明確に分かる例外クラスではなく RuntimeException を throw していたところでメッセージが出ていました。確かにその通りなので、今回はメッセージが出ている以下のクラスに @SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes") を付けることにします。

  • src/main/java/ksbysample/webapp/bootnpmgeb/helper/freemarker/FreeMarkerHelper.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/ValuesHelper.java
  • src/main/java/ksbysample/webapp/bootnpmgeb/values/validation/ValuesEnumValidator.java

Rather than using a lot of String arguments, consider using a container object for those values.

UseObjectForClearerAPI のルール違反のメッセージです。

メソッドの引数が多い場合に Parameter クラスを新規作成してそちらでも渡せるようにした方がよい、というルールのようです。今のところ必要性を感じなかったので exclude します。

The method '...' has a cyclomatic complexity of ....

CyclomaticComplexity のルール違反のメッセージです。

1メソッド内の行数(処理量?)が多いと出るメッセージで有効だと思うのですが、今回は exclude します。

以上で category/java/codestyle.xml は以下のような定義になりました。

    <rule ref="category/java/design.xml">
        <exclude name="LoosePackageCoupling"/>
        <exclude name="UseUtilityClass"/>
        <exclude name="NcssCount"/>
        <exclude name="LawOfDemeter"/>
        <exclude name="DataClass"/>
        <exclude name="UseObjectForClearerAPI"/>
        <exclude name="CyclomaticComplexity"/>
    </rule>

ここまでやってみた感想

今回は Rule の exclude 有無を1つずつ見直していますが、単純にバージョンアップするなら以下の方針で構わないと思います。

  • build.gradle の記述については特に変更する必要はありません。
  • 独自の Rulesets は書き方が変わっているので書き直しが必要。https://github.com/pmd/pmd/tree/master/pmd-java/src/main/resources/category/java の category の一覧を見て、xml ファイルに <rule ref="category/java/bestpractices.xml"/> のフォーマットで全て列挙します。
  • その後に build タスクを実行して、メッセージが出力された Rule を全て exclude します。
  • 6 以降に追加された Rule は exclude されたままにせず確認して追加するのか exclude したままにするのか判断します。

履歴

2018/06/03
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その54 )( webpack を 3.8.1 → 4.9.1 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その53 )( Gradle を 3.5 → 4.6 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • webpack を 3.8.1 → 4.9.1 へバージョンアップします。

参照したサイト・書籍

目次

  1. webpack を 3.8.1 → 4.9.1 へバージョンアップする
  2. npm run build コマンドを実行してみる
  3. webpack-cli をインストールする
  4. 再び npm run build コマンドを実行してみる
  5. uglifyjs-webpack-plugin を 1.2.2 → 1.2.5 へバージョンアップする
  6. webpack.config.js を変更する
  7. package.json を変更する
  8. cross-env をアンインストールする
  9. 動作確認
  10. webpack.config.js から optimization、devtool の設定を取り除くとどうなるのか?

手順

webpack を 3.8.1 → 4.9.1 へバージョンアップする

npm install --save-dev webpack@4.9.1 コマンドを実行します。

f:id:ksby:20180527204920p:plain

npm run build コマンドを実行してみる

バージョンアップしただけで webpack.config.js は何も修正していない状態で npm run build コマンドを実行してみます。

f:id:ksby:20180527211011p:plain

CLI が別のパッケージになったので webpack-cli か webpack-command をインストールするようメッセージが出力されました。webpack-cli と webpack-command の違いは webpack-command サイト内の Differences With webpack-cli に記述があります。

今回は webpack-cli をインストールします。

webpack-cli をインストールする

npm install --save-dev webpack-cli コマンドを実行します。

f:id:ksby:20180527211732p:plain

再び npm run build コマンドを実行してみる

再び npm run build コマンドを実行してみます。

f:id:ksby:20180527212056p:plain f:id:ksby:20180527212149p:plain

webpack 4 から追加された --mode オプションを指定していないので、WARNING のメッセージが出力されました。

webpack 3 → 4 へのマイグレーション関連の記事を Web で調べて、以下の点を変更することにします。

  • webpack を実行している npm scripts に --mode オプションを追加します。
  • cross-env パッケージが不要になるのでアンインストールします。
  • webpack 4 では --mode development が指定されている時には最適化処理が実行されず、--mode production が指定されている時には最適化処理が実行されるようになったので、Uglify の設定を変更します。
  • uglifyjs-webpack-plugin をインストールしなくても --mode production が指定されていると Uglify されるのですが、デフォルトの設定では console.log() が削除されないようなので、uglifyjs-webpack-plugin をインストールしたままにして独自に設定することにします。また uglifyjs-webpack-plugin を最新版にバージョンアップします。

uglifyjs-webpack-plugin を 1.2.2 → 1.2.5 へバージョンアップする

npm install --save-dev uglifyjs-webpack-plugin@1.2.5 コマンドを実行します。

f:id:ksby:20180528014124p:plain

webpack.config.js を変更する

webpack.config.js の以下の点を変更します。

const webpack = require("webpack");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

// --mode オプションで指定された文字列を参照したい場合には argv.mode を参照する
module.exports = (env, argv) => {
    return {
        entry: {
            "js/inquiry/input01": ["./src/main/assets/js/inquiry/input01.js"],
            "js/inquiry/input02": ["./src/main/assets/js/inquiry/input02.js"],
            "js/inquiry/input03": ["./src/main/assets/js/inquiry/input03.js"],
            "js/inquiry/confirm": ["./src/main/assets/js/inquiry/confirm.js"]
        },
        output: {
            path: __dirname + "/src/main/resources/static",
            publicPath: "/",
            filename: "[name].js"
        },
        resolve: {
            modules: [
                "node_modules",
                "src/main/assets/js"
            ],
            alias: {
                jquery: "jquery"
            }
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    exclude: [
                        /node_modules/,
                        /jquery.autoKana.js$/
                    ],
                    loader: "eslint-loader"
                }
            ]
        },
        optimization: {
            minimizer: [
                new UglifyJsPlugin({
                    uglifyOptions: {
                        compress: true,
                        ecma: 5,
                        output: {
                            comments: false
                        },
                        compress: {
                            dead_code: true,
                            drop_console: true
                        }
                    },
                    sourceMap: false
                })
            ]
        },
        plugins: [
            new webpack.ProvidePlugin({
                $: "jquery",
                jQuery: "jquery"
            })
        ],
        devtool: "inline-source-map"
    };
};
  • const isProduct = process.env.NODE_ENV === "product"; を削除します。
  • --mode オプションで指定されたモードを参照できるようにするために、module.exports = { ... };module.exports = (env, argv) => { return { ... }; }; に変更します。これで argv.mode--mode オプションで指定された文字列を参照できるようになります。
  • optimization: { ... } を追加し、ここに UglifyJsPlugin の設定を記述します。optimization: { ... } の設定は --mode development の時には適用されず(Uglify や sourceMap出力が行われません)、--mode production の時のみ適用されるようです(Uglify や sourceMap出力が行われます)。また ecma: 6 にすると IE11 で入力画面2の郵便番号を入力してヒットした候補をドロップダウンリストに表示する機能が動作しなかったので、ecma: 5 にします。
  • plugins: [ ... ] から .concat(isProduct ? [new UglifyJsPlugin()] : [] を削除します。
  • devtool: "inline-source-map" は残します。残しておかないとソースが変わりすぎて Chrome で debug しにくそうに思えたからです。optimization: { ... } で UglifyJsPlugin の設定も記述しておくと --mode production の時に sourceMap が残りません。

ちなみに console.log() が残ることを気にしなければ、optimization: { ... } の設定がなくても --mode production を指定すれば Uglify は行われます。

package.json を変更する

package.json の以下の点を変更します。

  "scripts": {
    ..........
    "webpack:build": "webpack --mode production",
    "webpack:watch": "webpack --mode development --watch",
    ..........
    "build": "run-s clean:cssjs-dir postcss:build webpack:build"
  },
  • webpack:build のコマンドを webpackwebpack --mode production に変更します。
  • webpack:watch のコマンドを webpack --watchwebpack --mode development --watch に変更します。
  • build のコマンドを cross-env NODE_ENV=product run-s ...run-s ... に変更します。

cross-env をアンインストールする

npm uninstall --save-dev cross-env コマンドを実行します。

f:id:ksby:20180529004519p:plain

動作確認

tomcat を起動した後、npm run springboot コマンドを実行してみます。

f:id:ksby:20180529012848p:plain f:id:ksby:20180529012900p:plain

生成された js ファイルを見るとファイルサイズが全て 700KB 以上あり、ファイルを開いてみても Uglify はされておらず、ソースはほぼそのまま残っています。

f:id:ksby:20180529013137p:plain

http://localhost:9080/inquiry/input/01/ にアクセスして入力画面1~3まで操作してみましたが、特に問題はありませんでした。

今度は npm run build コマンドを実行してみます。

f:id:ksby:20180529015031p:plain f:id:ksby:20180529015205p:plain

生成された js ファイルがほぼ 100KB 以下になり、ファイルを開いてみると Uglify されています。

f:id:ksby:20180529015339p:plain

http://localhost:8080/inquiry/input/01/ にアクセスして(ポート番号を 9080 → 8080 に変更して Tomcat に直接アクセスしています)入力画面1~3まで操作してみましたが、特に問題はありませんでした。

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy の失敗するテストをコメントアウトした後、clean タスク実行 → Rebuild Project → build タスクを実行してみます。

f:id:ksby:20180602124044p:plain

BUILD SUCCESSFUL のメッセージが出力されました。

問題なさそうですので、このまま 4.9.1 を使います。書き終わった時には 4.10.2 までバージョンが上がっていましたが。。。

webpack.config.js から optimization、devtool の設定を取り除くとどうなるのか?

optimization、devtool の設定を書かない時の動作もメモしておきます。

webpack.config.js から optimization、devtool の設定を削除します。

        ..........
        module: {
            rules: [
                {
                    test: /\.js$/,
                    exclude: [
                        /node_modules/,
                        /jquery.autoKana.js$/
                    ],
                    loader: "eslint-loader"
                }
            ]
        },
        plugins: [
            new webpack.ProvidePlugin({
                $: "jquery",
                jQuery: "jquery"
            })
        ]
    };
};

まずは --mode development である npm run springboot を実行してみます。

f:id:ksby:20180602125908p:plain f:id:ksby:20180602130010p:plain

optimization、devtool の設定を入れていた時は生成された js ファイルは 700KB以上ありましたが、今回は 300~400KB程度になりました。

生成された js ファイルを開いてみると、実行コードは改行コードが \r\n に変換されて eval( ... ); で囲まれるようです。

f:id:ksby:20180602130454p:plain

Tomcat を起動してから、Chromehttp://localhost:8080/inquiry/input/01/ にアクセスして DevTools で input01.js を開いてみると、eval( ... ); のまま表示されて、これだと debug をどうやればよいのかちょっと分かりませんでした。

f:id:ksby:20180602131630p:plain

sourceMap もたぶん出力されていませんね。ファイルサイズが小さいのはその辺が理由でしょう。

次に --mode production である npm run build を実行してみます。

f:id:ksby:20180602132306p:plain f:id:ksby:20180602132429p:plain

ファイルサイズは optimization、devtool の設定を入れていた時より数KBだけ大きいです。

生成された js ファイルを開いてみると、Uglify されていますがコメントが完全に削除されずに一部残っていました。

f:id:ksby:20180602132723p:plain

Tomcat を起動してから、Chromehttp://localhost:8080/inquiry/input/01/ にアクセスして動作確認してみましたが、IE11 でも入力画面2の郵便番号を入力してヒットした候補をドロップダウンリストに表示する機能が動作しましたし、他にも問題はありませんでした。

また console.log(...); も入れていると削除されません(これは実際に書いて試してみました)。

今のところ、個人的には optimization、devtool の設定は書く方が好みですね。webpack 4 の設定がまだよく分かっていないだけかもしれませんが。

履歴

2018/06/02
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その53 )( Gradle を 3.5 → 4.6 へバージョンアップする )

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • Gradle を 3.5 → 4.6 へバージョンアップします。

参照したサイト・書籍

目次

  1. build タスク実行時に出たエラーを解消する
    1. 'The String literal "セ??トされるはず?????ータがセ??トされて??ません" appears 5 times in this file; the first occurrence is on line 91'
  2. Gradle を 3.5 → 4.6 へバージョンアップする

手順

build タスク実行時に出たエラーを解消する

Gradle のバージョンアップの前に clean タスク実行 → Rebuild Project 実行 → build タスクを実行したところ、PMD の警告が出ていたので解消します。

f:id:ksby:20180527161213p:plain

'The String literal "セ??トされるはず?????ータがセ??トされて??ません" appears 5 times in this file; the first occurrence is on line 91'

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の中で throw new IllegalArgumentException("セットされるはずのデータがセットされていません"); とエラーメッセージの文字列を直接記述していたのが原因なので、定数文字列に変更します。

src/main/resources/messages_ja_JP.properties の以下の点を変更します。

InquiryInputController.validate.form.error=セットされるはずのデータがセットされていません
InquiryInput02Form.zipcode.UnmatchPattern=郵便番号が数字7桁ではありません。
..........
  • InquiryInputController.validate.form.error=セットされるはずのデータがセットされていません を追加します。

https://github.com/ksby/ksbysample-webapp-lending/blob/1.0.x/src/main/java/ksbysample/webapp/lending/helper/message/MessagesPropertiesHelper.java をコピーして src/main/java/ksbysample/webapp/bootnpmgeb/helper/message/MessagesPropertiesHelper.java に配置します。

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の以下の点を変更します。

@Slf4j
@Controller
@RequestMapping("/inquiry/input")
@SessionAttributes("sessionData")
public class InquiryInputController {

    ..........

    private static final String VALIDATE_FORM_ERROR = "InquiryInputController.validate.form.error";

    private final ModelMapper modelMapper;

    private final InquiryInput02FormValidator inquiryInput02FormValidator;

    private final Validator mvcValidator;

    private final MessagesPropertiesHelper mph;

    /**
     * コンストラクタ
     *
     * @param modelMapper                 {@link ModelMapper} オブジェクト
     * @param inquiryInput02FormValidator {@link InquiryInput02FormValidator} オブジェクト
     * @param mvcValidator                {@link Validator} オブジェクト
     * @param mph                         {@link MessagesPropertiesHelper} オブジェクト
     */
    public InquiryInputController(ModelMapper modelMapper
            , InquiryInput02FormValidator inquiryInput02FormValidator
            , Validator mvcValidator
            , MessagesPropertiesHelper mph) {
        this.modelMapper = modelMapper;
        this.inquiryInput02FormValidator = inquiryInput02FormValidator;
        this.mvcValidator = mvcValidator;
        this.mph = mph;
    }

    ..........

    /**
     * 入力画面1 「次へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/01", params = {"move=next"})
    public String input01MoveNext(@Validated InquiryInput01Form inquiryInput01Form
            , BindingResult bindingResult
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        if (bindingResult.hasErrors()) {
            bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode()));
            throw new IllegalArgumentException(mph.getMessage(VALIDATE_FORM_ERROR, null));
        }

        ..........
    }

    ..........
  • private static final String VALIDATE_FORM_ERROR = "InquiryInputController.validate.form.error"; を追加します。
  • private final MessagesPropertiesHelper mph; を追加します。
  • コンストラクタの引数に MessagesPropertiesHelper mph を、メソッド内に this.mph = mph; を追加します。
  • throw new IllegalArgumentException("セットされるはずのデータがセットされていません");throw new IllegalArgumentException(mph.getMessage(VALIDATE_FORM_ERROR, null)); に変更します(全部で5ヶ所)。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行して PMD の警告が出力されなくなったことを確認します(画面キャプチャは省略します)。

Gradle を 3.5 → 4.6 へバージョンアップする

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

task wrapper(type: Wrapper) {
    gradleVersion = '4.6'
}
  • gradleVersion = '3.5'gradleVersion = '4.6' に変更します。

コマンドプロンプトを起動し、gradlew wrapper コマンドを実行します。

f:id:ksby:20180527171636p:plain

gradle/wrapper/gradle-wrapper.properties を開くと gradle-4.6-bin.zip に変更されています。

f:id:ksby:20180527171841p:plain

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

必ず失敗させているテストが1つあるので無効にします。src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy の以下の点を変更します。

/*
        @Test
        void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、前の画面へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            expect:
            assert false, "確認画面を実装してからテストを作成する"
        }
*/
  • void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、前の画面へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() { ... } のテストをコメントアウトします。

clean タスク実行 → Rebuild Project → build タスクを実行します。。。が findbugsMain タスクでエラーになりました。

f:id:ksby:20180527173252p:plain

エラーが発生している Build file 'C:\project-springboot\ksbysample-boot-miscellaneous\boot-npm-geb-sample\build.gradle' line: 87 は以下の部分で、> No signature of method: org.gradle.api.internal.file.collections.DefaultConfigurableFileCollection.exclude() is applicable for argument types: (java.lang.String) values: [**/*.properties] というエラーメッセージが出ています。

f:id:ksby:20180527173523p:plain

tasks.withType(FindBugs) { ... }doFirst { ... } の部分はさすがに Gradle 4.x 以降になったら不要になっていると思いたいので、一旦削除してみます。

tasks.withType(FindBugs) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

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

f:id:ksby:20180527175209p:plain

コンソールに出力されていた https://docs.gradle.org/4.6/userguide/command_line_interface.html#sec:command_line_warnings を見ると、By default, Gradle won’t display all warnings (e.g. deprecation warnings). という記述がありました。これで FindBugs Plugin の warnings が出なくなっているようです。Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0. のメッセージが出ていますが、 5.0 にバージョンアップする時にまた考えることにします。

問題なさそうですので、このまま 4.6 を使います。src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy のコメントアウトしたテストは元に戻します。

履歴

2018/05/27
初版発行。

IntelliJ IDEA を 2018.1.2 → 2018.1.4 へバージョンアップ

IntelliJ IDEA を 2018.1.2 → 2018.1.4 へバージョンアップする

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

IntelliJ IDEA 2018.1.4 is released! の記事を読むと、Java SE を 8u162 → 8u172 へ、IntelliJ IDEA を 2017.3.4 → 2017.3.5 → 2018.1.2 へ、Git for Windows を 2.16.2 → 2.17.0 へバージョンアップ に書いた 「Working directory」に $MODULE_DIR$ がセットされるようになっていた件について変更が入ったようです。

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

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

  2. IDE and Plugin Updates」ダイアログが表示されます。マイナーバージョンアップなのに左下に「Update and Restart」ボタンではなく「Download」ボタンが表示されていますね。

    f:id:ksby:20180527095506p:plain

    「Download」ボタンを押して IntelliJ IDEA の Download ページを開いた後、「DOWNLOAD」ボタンを押して ideaIU-2018.1.4.exe をダウンロードします。

    f:id:ksby:20180527095722p:plain

  3. 起動している IntelliJ IDEA を終了します。

  4. ideaIU-2018.1.4.exe を実行します。

  5. IntelliJ IDEA Setup」ダイアログが表示されます。「Next >」ボタンをクリックします。

    f:id:ksby:20180527100953p:plain

  6. 「Uninstall old versions」画面が表示されます。画面上の全てのチェックボックスをチェックした後、「Next >」ボタンをクリックします。

    f:id:ksby:20180527101056p:plain

  7. 「Choose Install Location」画面が表示されます。「Destination Folder」を C:\IntelliJ_IDEA\2018.1.4 に変更した後、「Next >」ボタンをクリックします。

    f:id:ksby:20180527101215p:plain

  8. 「Installation Options」画面が表示されます。何も変更せずに「Next >」ボタンをクリックします。

    f:id:ksby:20180527101308p:plain

  9. 「Choose Start Menu Folder」画面が表示されます。何も変更せずに「Install」ボタンをクリックします。

  10. 「Installing」画面が表示されてインストールが始まりますので、完了するまで待ちます。

  11. インストールが完了すると「Completing IntelliJ IDEA Setup」画面が表示されます。「Finish」ボタンをクリックしてダイアログを閉じます。

  12. 今回は C:\IntelliJ_IDEA\2018.1.2 ディレクトリは削除されていました。

  13. C:\IntelliJ_IDEA\2018.1.4\bin\idea64.exe を実行します。

  14. IntelliJ IDEA のメイン画面が表示され画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20180527101906p:plain

  15. Plugin が全て最新にアップデートされていないようなので先にアップデートします。IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。

  16. IDE and Plugin Updates」ダイアログが表示されます。何も変更せずに「Update」ボタンをクリックします。

    f:id:ksby:20180527102035p:plain

    Patch がダウンロードされた後、IntelliJ IDEA を再起動します。再起動後、画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

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

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

    f:id:ksby:20180527102720p:plain

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

    f:id:ksby:20180527103552p:plain

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

    f:id:ksby:20180527104105p:plain

  21. 最後に「Working directory」に $MODULE_DIR$ がセットされる件を 新規Projectを作成して確認してみたところ、$MODULE_DIR$ がセットされたままでした。。。? セットされるのは変わらないようです。

    f:id:ksby:20180527105104p:plain

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成
    • サーバ側のテストを作成します。

参照したサイト・書籍

目次

  1. InquiryInput03FormNotEmptyRule クラスのテストを作成する
  2. InquiryInputController クラスのテストを変更する
  3. 次回は。。。

手順

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

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput03FormNotEmptyRule.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。

f:id:ksby:20180526172038p:plain

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput03FormNotEmptyRuleTest.groovy が新規作成されるので、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry.form

import ksbysample.webapp.bootnpmgeb.values.Type1Values
import ksbysample.webapp.bootnpmgeb.values.Type2Values
import spock.lang.Specification
import spock.lang.Unroll

import javax.validation.ConstraintViolation
import javax.validation.Validation

class InquiryInput03FormNotEmptyRuleTest extends Specification {

    def validator
    def inquiryInput03FormNotEmptyRule

    def setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator()
        inquiryInput03FormNotEmptyRule = new InquiryInput03FormNotEmptyRule(
                type1: Type1Values.PRODUCT.value
                , type2: [Type2Values.ESTIMATE.value, Type2Values.OTHER.value]
                , inquiry: "これはテストです"
                , survey: ["1", "2"]
        )
    }

    @Unroll
    def "type1 の NotEmpty のテスト(#type1 --> #size)"() {
        setup:
        inquiryInput03FormNotEmptyRule.type1 = type1
        Set<ConstraintViolation<InquiryInput03FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput03FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        type1                     || size
        ""                        || 1
        Type1Values.PRODUCT.value || 0
    }

    @Unroll
    def "type2 の NotEmpty のテスト(#type2 --> #size)"() {
        setup:
        inquiryInput03FormNotEmptyRule.type2 = type2
        Set<ConstraintViolation<InquiryInput03FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput03FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        type2                                                                              || size
        []                                                                                 || 1
        [Type2Values.ESTIMATE.value]                                                       || 0
        [Type2Values.ESTIMATE.value, Type2Values.CATALOGUE.value, Type2Values.OTHER.value] || 0
    }

    @Unroll
    def "inquiry の NotEmpty のテスト(#inquiry --> #size)"() {
        setup:
        inquiryInput03FormNotEmptyRule.inquiry = inquiry
        Set<ConstraintViolation<InquiryInput03FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput03FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        inquiry || size
        ""      || 1
        "テスト"   || 0
    }

}

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

f:id:ksby:20180526181552p:plain

InquiryInputController クラスのテストを変更する

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy の以下の点を変更します。

@RunWith(Enclosed)
class InquiryInputControllerTest {

    ..........

    @RunWith(SpringRunner)
    @SpringBootTest
    static class 入力画面3のテスト {
        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))
        private InquiryInput02Form inquiryInput02Form_001 = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
        private InquiryInput03Form inquiryInput03Form_001 = new InquiryInput03Form(
                type1: Type1Values.PRODUCT.value
                , type2: [Type2Values.ESTIMATE.value, Type2Values.CATALOGUE.value, Type2Values.OTHER.value]
                , inquiry: "これはテストです"
                , survey: ["1", "2", "3", "4", "5", "6", "7", "8"])

        @Autowired
        private WebApplicationContext context

        MockMvc mockMvc

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .build()
        }

        @Test
        void "初期表示時は画面の項目には何もセットされない"() {
            expect:
            mockMvc.perform(get("/inquiry/input/03"))
                    .andExpect(status().isOk())
                    .andExpect(html("select[name='type1'] option[selected]").notExists())
                    .andExpect(html("input[name='type2'][checked='checked']").notExists())
                    .andExpect(html("#inquiry").val(""))
                    .andExpect(html("input[name='survey'][checked='checked']").notExists())
        }

        @Test
        void "項目全てに入力して前の画面へ戻るボタンをクリックすると入力画面2へ戻り、次へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で項目全てに入力して「前の画面へ戻る」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=back", inquiryInput03Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))

            and: "入力画面2で「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            then: "入力画面3が以前入力したデータがセットされて表示される"
            mockMvc.perform(get("/inquiry/input/03").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("select[name='type1'] option[selected]").val(inquiryInput03Form_001.type1))
                    .andExpect(html("input[name='type2'][checked='checked']").count(3))
                    .andExpect(html("#inquiry").val(inquiryInput03Form_001.inquiry))
                    .andExpect(html("input[name='survey'][checked='checked']").count(8))
        }

        @Test
        void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、前の画面へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            expect:
            assert false, "確認画面を実装してからテストを作成する"
        }

        @Test
        void "入力チェックエラーのあるデータで「次へ」ボタンをクリックするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput03Form_001.type1 = ""

            expect: "入力画面3の「次へ」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

}
  • 入力画面3のテスト クラスを追加します。
  • 項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、前の画面へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される のテストは確認画面を実装しないとテストを作成できなかったので、今は常に失敗するようにします。

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

f:id:ksby:20180526193719p:plain

次回は。。。

確認画面を作成するか、以下3つのどれかをやりたいと思います。

履歴

2018/05/26
初版発行。

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成
    • 「前の画面へ戻る」ボタン、「確認画面へ」ボタンが正常に動作していないので原因を調査します。
    • その後にサーバ側の処理を実装します。

参照したサイト・書籍

目次

  1. 「前の画面へ戻る」ボタンを押すと固まる原因を調査する
  2. 「確認画面へ」ボタンを押すとエラーになる原因を調査する
  3. Form クラスを作成する
  4. input03.html, input03.js を変更する
  5. SessionData クラスを変更する
  6. 画面表示時と「前の画面へ戻る」「確認画面へ」ボタンクリック時の処理を実装する
  7. 動作確認

手順

「前の画面へ戻る」ボタンを押すと固まる原因を調査する

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その41 )( IntelliJ IDEA で Javascript を debug する ) に書いた手順で debug してみたら form タグの id 属性の文字列が input03.js に書いている id と一致していないことが原因でした。

src/main/assets/js/inquiry/input03.js を以下のように変更します。

          <form id="inquiryInput03Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/03/}">
  • input03ForminquiryInput03Form に変更します。

入力画面3を表示して「前の画面へ戻る」ボタンを押すと、今度は入力画面2に戻りました。

f:id:ksby:20180509223058p:plain f:id:ksby:20180509223402p:plain

「確認画面へ」ボタンを押すとエラーになる原因を調査する

上の調査で debug していた時に気づきましたが、原因は「確認画面へ」ボタンの class 属性に記述しているクラス名が js-btn-confirm なのに input03.js に書いているクラス名が js-btn-next だったからでした。

src/main/assets/js/inquiry/input03.js を以下のように変更します。

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    ..........

    // 「前の画面へ戻る」「次へ」ボタンをクリック不可にする
    $(".js-btn-back").prop("disabled", true);
    $(".js-btn-confirm").prop("disabled", true);

    ..........
};

$(document).ready(function () {
    ..........

    // 「前の画面へ戻る」「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-back").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=back", true);
    });
    $(".js-btn-confirm").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=next", false);
    });

    ..........
});
  • .js-btn-next.js-btn-confirm に変更します。

動作確認します。入力画面3を表示して、

f:id:ksby:20180509230213p:plain

何も選択・入力せずに「確認画面へ」ボタンを押すと、入力画面3のままで必須項目にエラーメッセージが表示されました。

f:id:ksby:20180509230338p:plain

必須項目全てに選択・入力してから「確認画面へ」ボタンを押すと、

f:id:ksby:20180509230451p:plain

今度は確認画面が表示されました。

f:id:ksby:20180509230707p:plain

Form クラスを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput03Form.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry.form;


import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * 入力画面3用 Form クラス
 */
@Data
public class InquiryInput03Form implements Serializable {

    private static final long serialVersionUID = -2818250124844174764L;

    private String type1;

    private List<String> type2 = new ArrayList<>();

    private String inquiry;

    private List<String> survey;

    private boolean copiedFromSession = false;

}

必須チェックだけ実行するためのクラスも作ります。src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form の下に InquiryInput03FormNotEmptyRule.java を新規作成し、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry.form;


import lombok.Data;
import org.hibernate.validator.constraints.NotEmpty;

import java.util.ArrayList;
import java.util.List;

/**
 * 入力画面3 必須チェック用クラス
 */
@Data
public class InquiryInput03FormNotEmptyRule {

    @NotEmpty
    private String type1;

    @NotEmpty
    private List<String> type2 = new ArrayList<>();

    @NotEmpty
    private String inquiry;

    private List<String> survey;

    private boolean copiedFromSession = false;

}

input03.html, input03.js を変更する

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

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}">
  <title>入力フォーム - 入力画面3</title>

  <style>
    /* 「チェックボックス複数行」の入力項目のチェックボックスを複数行に書くので、 */
    /* 異なる行のチェックボックスの位置を左揃えにする                         */
    @media (min-width: 768px) {
      #multiline-checkbox .checkbox label {
        display: block;
        float: left;
        width: 180px;
      }
    }
    @media (max-width: 767px) {
      #multiline-checkbox .checkbox label {
        display: block;
        float: left;
        width: 100%;
      }
    }
  </style>
</head>

<body class="skin-blue layout-top-nav">
<div class="wrapper">

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <!-- Content Header (Page header) -->
    <section class="content-header">
      <h1>
        入力画面3
      </h1>
    </section>

    <!-- Main content -->
    <section class="content">
      <div class="row">
        <div class="col-xs-12">
          <!--/*@thymesVar id="inquiryInput03Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form"*/-->
          <form id="inquiryInput03Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/03/}"
                th:object="${inquiryInput03Form}">
            <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/>

            <!-- お問い合わせの種類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}"
                        th:field="*{type1}">
                      </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 -->
            <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:field="*{type2}">
                        <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>

            <!-- お問い合わせの内容 -->
            <div class="form-group" id="form-group-inquiry">
              <div class="control-label col-sm-2">
                <label class="float-label">お問い合わせの内容</label>
                <div class="label label-required">必須</div>
              </div>
              <div class="col-sm-10">
                <div class="row"><div class="col-sm-10">
                  <textarea rows="5" name="inquiry" id="inquiry" class="form-control" maxlength="500" placeholder="お問い合わせ内容を入力して下さい"
                    th:field="*{inquiry}"></textarea>
                </div></div>
                <div class="row"><div class="col-sm-10"><p class="form-control-static"><small>※最大500文字</small></p></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>

            <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:field="*{survey}">
                    <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 class="text-center">
              <button class="btn bg-blue js-btn-back"><i class="fa fa-arrow-left"></i> 前の画面へ戻る</button>
              <button class="btn bg-green js-btn-confirm"><i class="fa fa-arrow-right"></i> 確認画面へ</button>
            </div>
          </form>
        </div>
      </div>
    </section>
    <!-- /.content -->
  </div>
  <!-- /.content-wrapper -->
</div>
<!-- ./wrapper -->

<!-- REQUIRED JS SCRIPTS -->
<script src="/js/inquiry/input03.js"></script>

</body>
</html>
  • form タグの上に <!--/*@thymesVar id="inquiryInput03Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form"*/--> を追加します。
  • form タグの末尾に th:object="${inquiryInput03Form}" を追加します。
  • <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/> を追加します。
  • <select name="type1" id="type1" class="form-control" style="width: 250px;" autofocus><select name="type1" id="type1" class="form-control" style="width: 250px;"> に変更します。
  • 入力/選択項目のタグに th:field="*{...}"(... には入力項目に対応した変数を記述) を追加します。

src/main/assets/js/inquiry/input03.js の以下の点を変更します。

$(document).ready(function (event) {
    // 入力チェック用の validator 関数をセットする
    $("#type1").on("blur", type1Validator);
    $("input:checkbox[name='type2']").on("blur", type2Validator);
    $("#inquiry").on("blur", inquiryValidator);

    // 「前の画面へ戻る」「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-back").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=back", true);
    });
    $(".js-btn-confirm").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=next", false);
    });

    // 初期画面表示時にセッションに保存されていたデータを表示する場合には
    // 入力チェックを実行して画面の表示を入力チェック後の状態にする
    if ($("#copiedFromSession").val() === "true") {
        executeAllValidator(event);
    }

    // 「お問い合わせの種類1」にフォーカスをセットする
    $("#type1").focus().select();
});
  • $(document).ready(function () {$(document).ready(function (event) { に変更します。
  • if ($("#copiedFromSession").val() === "true") { ... } を追加します。

SessionData クラスを変更する

src/main/java/ksbysample/webapp/bootnpmgeb/session/SessionData.java の以下の点を変更します。

@Data
public class SessionData implements Serializable {

    private static final long serialVersionUID = -2673191456750655164L;

    private InquiryInput01Form inquiryInput01Form;

    private InquiryInput02Form inquiryInput02Form;

    private InquiryInput03Form inquiryInput03Form;

}
  • private InquiryInput03Form inquiryInput03Form; を追加します。

画面表示時と「前の画面へ戻る」「確認画面へ」ボタンクリック時の処理を実装する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputController.java の以下の点を変更します。

@Slf4j
@Controller
@RequestMapping("/inquiry/input")
@SessionAttributes("sessionData")
public class InquiryInputController {

    ..........

    /**
     * 入力画面3 初期表示処理
     *
     * @return 入力画面3の Thymeleaf テンプレートファイルのパス
     */
    @GetMapping("/03")
    public String input03(InquiryInput03Form inquiryInput03Form
            , SessionData sessionData) {
        // セッションに保存されているデータがある場合にはコピーする
        if (sessionData.getInquiryInput03Form() != null) {
            modelMapper.map(sessionData.getInquiryInput03Form(), inquiryInput03Form);
            inquiryInput03Form.setCopiedFromSession(true);
        }

        return TEMPLATE_INPUT03;
    }

    /**
     * 入力画面3 「前へ」ボタンクリック時の処理
     *
     * @return 入力画面2の URL
     */
    @PostMapping(value = "/03", params = {"move=back"})
    public String input03MoveBack(@Validated InquiryInput03Form inquiryInput03Form
            , BindingResult bindingResult
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        if (bindingResult.hasErrors()) {
            bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode()));
            throw new IllegalArgumentException("セットされるはずのデータがセットされていません");
        }

        // 入力されたデータをセッションに保存する
        sessionData.setInquiryInput03Form(inquiryInput03Form);

        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_INPUT_02).toUriString();
    }

    /**
     * 入力画面3 「確認画面へ」ボタンクリック時の処理
     *
     * @return 確認画面の URL
     */
    @PostMapping(value = "/03", params = {"move=next"})
    public String input03MoveNext(@Validated InquiryInput03Form inquiryInput03Form
            , BindingResult bindingResult
            , InquiryInput03FormNotEmptyRule inquiryInput03FormNotEmptyRule
            , SessionData sessionData
            , UriComponentsBuilder builder) {
        // 必須チェックをする
        mvcValidator.validate(inquiryInput03FormNotEmptyRule, bindingResult);
        if (bindingResult.hasErrors()) {
            bindingResult.getAllErrors().stream().forEach(e -> log.warn(e.getCode()));
            throw new IllegalArgumentException("セットされるはずのデータがセットされていません");
        }

        // 入力されたデータをセッションに保存する
        sessionData.setInquiryInput03Form(inquiryInput03Form);

        return UrlBasedViewResolver.REDIRECT_URL_PREFIX
                + builder.path(UrlConst.URL_INQUIRY_CONFIRM).toUriString();
    }

}
  • input03 メソッドの以下の点を変更します。
    • 引数に InquiryInput03Form inquiryInput03FormSessionData sessionData を追加します。
    • if (sessionData.getInquiryInput02Form() != null) { ... } の処理を追加します。
  • input03MoveBack メソッドの以下の点を変更します。
    • 引数に @Validated InquiryInput03Form inquiryInput03FormBindingResult bindingResultSessionData sessionData を追加します。
    • if (bindingResult.hasErrors()) { ... } の処理を追加します。
    • sessionData.setInquiryInput03Form(inquiryInput03Form); を追加します。
  • input03MoveNext メソッドの以下の点を変更します。
    • 引数に @Validated InquiryInput03Form inquiryInput03FormBindingResult bindingResultInquiryInput03FormNotEmptyRule inquiryInput03FormNotEmptyRuleSessionData sessionData を追加します。
    • mvcValidator.validate(inquiryInput03FormNotEmptyRule, bindingResult); を追加します。
    • if (bindingResult.hasErrors()) { ... } を追加します。
    • sessionData.setInquiryInput03Form(inquiryInput03Form); を追加します。

動作確認

動作確認します。npm run springboot コマンドを実行し Tomcat を起動した後、ブラウザで http://localhost:9080/inquiry/input/03/ にアクセスします。

データを入力してから、「次へ」ボタンをクリックして入力画面3へ遷移します。

f:id:ksby:20180519180336p:plain

何も入力せずに「前の画面へ戻る」ボタンをクリックすると、入力画面2へ戻ります。サーバ側でも必須チェックは行われません。

f:id:ksby:20180519180651p:plain

「次へ」ボタンをクリックして入力画面3へ戻った後、データを入力します。

f:id:ksby:20180519180943p:plain

「前の画面へ戻る」ボタンをクリックして入力画面2へ戻ってから、

f:id:ksby:20180519181410p:plain

「次へ」ボタンをクリックして入力画面3へ戻ると、前に入力したデータが表示されます。

f:id:ksby:20180519181723p:plain

入力したデータを変更してから、

f:id:ksby:20180519182114p:plain

「確認画面へ」ボタンをクリックして確認画面へ遷移した後、

f:id:ksby:20180519182247p:plain

一番下の「修正する」ボタンをクリックすると、入力画面3へ戻り変更したデータが表示されます。

f:id:ksby:20180519182600p:plain

問題なく動作しているようです。

履歴

2018/05/19
初版発行。

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

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • 入力画面3の作成
    • Javascript の処理を実装します。

参照したサイト・書籍

目次

  1. 入力チェックを実装する
  2. input03.html を修正する
  3. 動作確認

手順

入力チェックを実装する

src/main/assets/js/inquiry/input03.js を以下のように変更します。

"use strict";

var Form = require("lib/class/Form.js");
var validator = require("lib/util/validator.js");

var form = new Form([
    "#type1",
    "input:checkbox[name='type2']",
    "#inquiry",
    "input:checkbox[name='survey']"
]);

var type1Validator = function (event) {
    var idFormGroup = "#form-group-type1";
    var idList = ["#type1"];
    form.convertAndValidate(form, event, idFormGroup, idList,
        undefined,
        function () {
            validator.checkRequired(form, idFormGroup, idList, "お問い合わせの種類1を選択してください");
        }
    );
};

var type2Validator = function (event) {
    var idFormGroup = "#form-group-type2";
    var idList = ["input:checkbox[name='type2']"];
    form.convertAndValidate(form, event, idFormGroup, idList,
        undefined,
        function () {
            validator.checkRequired(form, idFormGroup, idList, "お問い合わせの種類2を選択してください");
        }
    );
};

var inquiryValidator = function (event) {
    var idFormGroup = "#form-group-inquiry";
    var idList = ["#inquiry"];
    form.convertAndValidate(form, event, idFormGroup, idList,
        undefined,
        function () {
            validator.checkRequired(form, idFormGroup, idList, "お問い合わせの内容を入力してください");
        }
    );
};

var executeAllValidator = function (event) {
    form.forceAllFocused(form);
    [
        type1Validator,
        type2Validator,
        inquiryValidator
    ].forEach(function (validateFunction) {
        validateFunction(event);
    });
};

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    // 全ての入力チェックを実行する
    try {
        if (ignoreCheckRequired) {
            validator.ignoreCheckRequired = ignoreCheckRequired;
            form.backupFocusedState(form);
        }
        executeAllValidator(event);
    } finally {
        if (ignoreCheckRequired) {
            validator.reset();
            form.restoreFocusedState(form);
        }
    }
    // 入力チェックエラーがある場合には処理を中断する
    if (event.isPropagationStopped()) {
        // 一番最初のエラーの項目にカーソルを移動する
        $(".has-error:first :input:first").focus().select();
        return false;
    }

    // 「前の画面へ戻る」「次へ」ボタンをクリック不可にする
    $(".js-btn-back").prop("disabled", true);
    $(".js-btn-next").prop("disabled", true);

    // サーバにリクエストを送信する
    $("#ignoreCheckRequired").val(ignoreCheckRequired);
    $("#inquiryInput03Form").attr("action", url);
    $("#inquiryInput03Form").submit();

    // return false は
    // event.preventDefault() + event.stopPropagation() らしい
    return false;
};

$(document).ready(function () {
    // 入力チェック用の validator 関数をセットする
    $("#type1").on("blur", type1Validator);
    $("input:checkbox[name='type2']").on("blur", type2Validator);
    $("#inquiry").on("blur", inquiryValidator);

    // 「前の画面へ戻る」「次へ」ボタンクリック時の処理をセットする
    $(".js-btn-back").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=back", true);
    });
    $(".js-btn-next").on("click", function (e) {
        return btnBackOrNextClickHandler(e, "/inquiry/input/03/?move=next", false);
    });

    // 「お問い合わせの種類1」にフォーカスをセットする
    $("#type1").focus().select();
});

input03.html を修正する

「次へ」ボタンはいつでも押せるようにします。src/main/resources/templates/web/inquiry/input02.html を以下のように変更します。

            <div class="text-center">
              <button class="btn bg-blue js-btn-back"><i class="fa fa-arrow-left"></i> 前の画面へ戻る</button>
              <button class="btn bg-green js-btn-confirm"><i class="fa fa-arrow-right"></i> 確認画面へ</button>
            </div>
  • <button class="btn bg-green js-btn-confirm" disabled><button class="btn bg-green js-btn-confirm"> へ変更します。

動作確認

npm run springboot を実行し、Tomcat を起動して http://localhost:9080/inquiry/input/03/ にアクセスすると入力画面3が表示されます。

f:id:ksby:20180509005649p:plain

何も入力せずに Tab キーでカーソルを「前の画面へ戻る」ボタンまで移動すると赤色になってエラーメッセージが表示されます。

f:id:ksby:20180509005833p:plain

F5 キーを押してリロードした後、全ての項目にエラーにならないようデータを選択・入力すると、「アンケート」以外の項目が緑色になります。

f:id:ksby:20180509010805p:plain

Javascript の入力チェックは正常に動作しているようです。

ただし「前の画面へ戻る」ボタンを押すと入力画面3のまま固まって入力画面2へは戻らず、「確認画面へ」ボタンを押すとエラーページが表示されました(入力データは保存しないが画面遷移だけはしたはずだった気が。。。) 次回は画面遷移しない原因を調べてみます。

f:id:ksby:20180509011330p:plain

履歴

2018/05/09
初版発行。