開発プロジェクトにおける main.js の使い方

Alier の開発プロジェクトの app_res/app_res/main.js(旧 DemoList.js, S3_1 マージ以前)では、ソースの先頭で testList というオブジェクトを定義しています。testList は、テストコードのファイル名とテストコードの説明のペアを要素とするリストです。

テストコードを追加したい場合、testList にテストコードのファイル名と説明のペアを追加します。testList 中のファイル名と説明はテストアプリ起動時に、ファイル名の書かれたボタンと説明文として画面に表示される。ボタンをクリックすることでサンプルコードが実行されます。

テストコードの実行は次のステップで行われます:

  1. テストコードのソースファイルを Alier.import() によりインポートする
  2. インポートした名前空間オブジェクトを globalThis へコピーし、エクスポートされた機能をグローバルスコープから参照できるようにする
  3. テストコードのエントリポイント関数を実行する

テストコード自体の実装方法については次の節で詳述します。


テストアプリの実装方法

テストアプリは以下のファイルで構成されます:

  • スクリプトファイル (.js)
  • (*) コンテンツを記述した HTML ファイル (.html)
  • (*) HTML ファイルから参照する CSS ファイル (.css)
  • (*) モデルインタフェースを定義する XML ファイル (.xml)
  • (*) その他、画像などの必要なリソースファイル

星印 (*) で示したものに関しては、必要な場合にのみ用意してください。最低限、必須なものはスクリプトファイルのみです。

スクリプトファイルとして記述が必要なものは以下の通りです:

  1. async function running(auto, screen)
    • テストアプリのエントリポイントとなる関数
    • 外部から実行可能にするため、スクリプトファイル内で Alier.export() によりエクスポートされていなければならない
  2. ViewLogic の派生クラス
    • テストアプリの画面制御やイベント処理を記述するクラス
    • インスタンスにテスト用のコンテンツを持たせるためのコンテナ

(2) の ViewLogic 派生クラスは、モジュールファイルのトップレベルで定義しても問題ないですが、通常は (1) の running() の外部からは参照されないため、running() の中でクラス定義を行う方が適切です。いずれにせよ、running() の実行時までに (2) のクラス定義が行われ、インスタンス生成ができるように実装しなければならない。

async function running(auto, screen) {
    /* テストアプリの起動時処理 1:
     * vl から参照されるオブジェクトをここで定義する
     */

    //  class 式に直接 new 演算子を作用させるパターン;
    //  クラスの実態はコンストラクタ関数であり、無名関数を作れるのと同じく無名クラスを作れる;
    //  static メンバを定義していたりコンストラクタの .name プロパティを参照する必要がなければ、
    //  無名クラスとして定義するほうが簡便
    const vl = new extends ViewLogic {
      /* ViewLogic 派生クラスの実装 */
    };

    /* テストアプリの起動時処理 2:
     * vl のクラス定義の中で起動時処理を記述できるのでここにはほとんど何も書かない
     */
}

await Alier.export({ running });  // running をエクスポート

コンテンツの作成と表示について

基底の ViewLogic ではコンストラクタ呼び出し時点でインスタンスにコンテンツを設定しないため、テストアプリで ViewLogic 派生クラスを定義する際、そのインスタンスに格納するコンテンツを用意する必要があります。ViewLogic へのコンテンツの提供方法は2通りあります:

  • HTML ファイルとして提供する: ViewLogic::loadContainer() で HTML ファイルを読み込む。

    const vl = new extends ViewLogic {
        constructor() {
            super();
            //  1.  loadContainer():
            //      "my-container.html" を読み込み、パース結果から id が "container-id" の要素を取り出す; 結果を container に設定し、またそれを返値として返す。
            //  2.  collectElements():
            //      引数で受け取った Promise から Element を取得する; 取得した Element から data-ui-component カスタムデータ属性と id 属性を持つ要素を抽出する。
            //  3.  relateElements():
            //      collectElements() で抽出した要素を対象の ViewLogic のプロパティに設定する。
            //  collectElements(), relateElements() は Promise をうけとったとき、その解決を待って処理を行う。
            this.relateElements(this.collectElements(this.loadContainer({ file: "my-container.html", id: "container-id" }))).then(() => {
                this.post(this.message("init"));    //  初期化完了の通知を自分自身へ送る
            });
        }
    };
  • HTML 文字列として提供する: ViewLogic::loadContainer() に HTML 文字列を渡す。

    const vl = new extends ViewLogic {
        constructor() {
            super();
            //  HTML 文字列をパースして指定した id 属性を持つ要素をコンテンツに設定する
            this.loadContainer({ text: container_html_string, id: container_id });
    
            const ui = this.collectElements(this.container);  //  data-ui-component 属性の定義された要素をコンテンツから収集する
            this.relateElements(ui);                         //  ui を自身に関連付ける
    
            this.post(this.message("init"));                 //  初期化完了の通知を自分自身へ送る
        }
    };

HTML ファイルを読み込む方法では、別途 HTML ファイル(上記の実装例では my-container.html にあたる)を作成する必要がある。

通常、コンテンツをインスタンス生成後に差し替えることはないので、コンテンツの設定は派生クラスのコンストラクタの中で行います。

設定したコンテンツを画面へ表示するには、アプリ画面 Alier.View に対して生成した ViewLogicViewElement::attach() する。 attach() とコンテンツ設定の順序はどちらが先でもよい。

  • 初期化後に attach() する場合:

    const vl = new extends ViewLogic {
        constructor() {
            super();
            this.relateElements(this.collectElements(this.loadContainer({ file: "my-container.html", id: "container-id" }))).then(container => {
                this.post(this.message("init"));  // 初期化完了通知
            });
        }
        async messageHandler(msg) {
            msg.deliver({
                //  初期化完了時の処理
                init: (msg) => {
                    Alier.View.attach(this);  //  初期化完了後に Alier.View へ attach する
                }
            });
        }
    };
  • 初期化前に attach() する場合:

    const vl = new extends ViewLogic {
        constructor() {
            super();
            this.relateElements(this.collectElements(this.loadContainer({ file: "my-container.html", id: "container-id" }))).then(container => {
                this.post(this.message("init"));  // 初期化完了通知
            });
        }
    };
    Alier.View.attach(vl);  //  初期化完了を待たずに attach

Back ボタンの実装

テストアプリはトップ画面とトップ画面から遷移できる各アプリ画面で構成されます。遷移先のアプリ画面からトップ画面へ戻る処理は、アプリ画面側で実装する必要があります。

典型的には Back ボタンを配置し、Back ボタンのクリックイベントを起点としてトップ画面への画面遷移を行います。トップ画面のコンテンツとして指定されていた ViewLogicrunning(auto, screen) の第2引数 screen として渡されます。したがって Back ボタンの実装では Alier.View.attach(screen) を評価するのみです。

async function running(auto, screen) {
    const vl = new extends ViewLogic {

        constructor() {
                super();
                this.relateElements(this.collectElements(this.loadContainer({ file: "my-container.html", id: "container-id" }))).then(container => {
                    this.post(this.message("init"));  // 初期化完了通知
                });
            }

            async messageHandler(msg) {
                msg.deliver({
                    //  Back ボタンの実装
                    back: (msg) => {
                        // アプリ画面を screen に切り替える
                        Alier.View.attach(screen);
                    }
                });
        }
    };
}

コンテンツ(HTML)側の実装は以下のようになります:

<div id="container-id">
  <button
    type="button"
    id="back"
    data-ui-component
    data-ui-active-events="click"
    value="back"
  >
    Back
  </button>
</div>

上記のように定義された <button> 要素は ProtoViewLogic::collectElements() によって収集され、また ProtoViewLogic::relateElements() によって ViewLogic に関連付けられます。関連付けられた要素からクリックイベントなどの UI 操作のイベントがメッセージとして ViewLogic に送られるようになります。送られたメッセージは ProtoViewLogic::messageHandler() を通じて処理できます。UI 操作の通知メッセージは id として対象の要素の id 属性値が指定されます。


自動テストの実装

トップ画面内最下部の autoTest ボタンを押すと、自動でテストコードが実行されます。

running(auto, screen) の引数 auto: booleantrue の場合、自動テストが実行されていることを示します。autotrue だった場合に実行すべき一連のテストコードを用意することで、テストアプリやテストアプリで使用している Alier フレームワークのモジュールの正常性を確認できます。

自動テストにおいてテストアプリは以下のことを行うよう実装される必要があります:

  1. テストコードの実行
  2. テストコードで発生した例外の検出と事後処理
  3. テストコード内で非同期に実行されている処理の待ち合わせ
  4. テストコードの実行が終了した時点で、アプリのトップ画面へ画面を切り替える

(3) は特に重要であり、未解決の非同期処理が残った状態でトップ画面への遷移を行うと、他のテストアプリの自動実行に影響し、全テストケースが期待される事前条件の元で実行されなくなります。

上記の (2) -- (4) は定型的な処理のため、例えば下記のように抽象化できます:

async function running(auto, screen) {
    const vl = new extends ViewLogic {

        //  処理待ちのタスクたち
        tasks = new Set();

        async messageHandler(msg) {
            msg.deliver({
                init: (msg) => {
                    if (auto) {
                        return this.post(this.message("test"));
                    }
                },
                back: (msg) => {
                    Alier.View.attach(screen);
                },
                test: (msg) => {
                    return this.test();
                },
                someTask: (msg) => {
                    let handled = false;
                    let resolve, reject;

                    //  新しい待ち受けオブジェクト (Promise) を生成
                    const task = new Promise((resolve_, reject_) => {
                        resolve = resolve_;
                        reject  = reject_;
                    });

                    //  待ち受けオブジェクトを追加する
                    this.tasks.add(task);

                    /* 何かの処理 */

                    //  待ち受けを解決する
                    resolve(handled);
                    return task;
                }
            });
        }

        async waitAllTasksSettled() {
            await Promise.all([...this.tasks]); //  実行待ちの全タスクの待ち合わせ
            this.tasks.clear();                 //  tasks を初期化
        }

        async test() {
            //  実行中のタスクがない状態でテストコードの実行を開始する
            await this.waitAllTasksSettled();

            //  テストコードの実装を呼び出す
            await this.testImpl();

            //  実行中のタスクの完了を待つ
            await this.waitAllTasksSettled();

            return this.post(this.message("back"));
        }

        async testImpl() {
            /* テストコードの実装 */
        }
    };
}

サンプルコードの実装上の注意点

サンプルコード固有の制約事項として、サンプルコードのエントリポイントは(main() ではなく)running() 関数として定義され、また Alier.export() によりエクスポートされている必要があります。また同様に、(running() 以外の)サンプルコード内部で定義されたクラスや関数などの機能を必要とする場合、それらもエクスポートする必要があります。エクスポートを忘れてグローバルスコープにそれらの機能が定義されている前提のスクリプトを実行した場合、参照エラーによってスクリプトの実行が中断されます。

ボタンクリックによってサンプルコードを実行する際、画面の末尾に Back ボタンが追加されます。Back ボタンをクリックすることで、テストアプリのメイン画面(サンプルコードの一覧画面)に戻ることができます。Back ボタンをクリックした際、サンプルコードの実行時にインポートした機能はグローバルスコープから削除されます(これは複数のサンプルを実行する際に、グローバルの名前空間が汚染された状態で後続のサンプルが実行されることを防ぐために行われる)。