Inject JS Scriptで呼び出したJavaScriptの非同期処理をUiPathに待たせる方法

UiPath (ja) Advent Calendar ２度目の登場です。すみませんすみません。

[2日目](https://qiita.com/soten_bluesky/items/71636a4a8c29fcd47259)では、UiPathでJavaScriptを活用する方法を長々と書かせていただきました。
もし、UiPathでJavaScriptを活用するイメージにまだピンと来ていないようでしたらそちらからお読みください。

## TL;DR

- いつ処理が終わるかわからないことを非同期処理と呼ぶ（正確な説明は割愛します）
- `Inject JS Script`アクティビティで実行したJavaScriptの非同期処理をUiPath側で認識するために、`Find Element`（要素を探す）アクティビティなどを利用する
- UiPath側に「処理が終わったことを認識させるための要素」をJavaScriptで作り出してDOMに出力する
    - UiPath側が要素を発見できれば、それは非同期処理の終了を検知したことと同義である

## はじめに

- UiPathにJavaScriptの非同期処理終了を認識させる時のお話です
- Webサイトの中に登場するUIのことを、本記事では**要素**と呼称します
- `Inject JS Script`（JS スクリプトを挿入）アクティビティを利用します
    - [Inject JS Scriptのドキュメント](https://activities.uipath.com/lang-ja/docs/inject-js-script)
- 2日目の記事同様、この記事はJavaScriptメインの記事ではなくUiPathで難しいことをJavaScriptで解決しよう、という記事です
- RPAビギナーによる記事のため、誤りやより高速で安全な方法がありまいたらご指摘ください
- 対象読者は、UiPathでJavaScriptを活用しているRPAエンジニアの皆さんです


## 非同期処理とは

通常JavaScriptコードは、書かれた順に必ず動作します。

```javascript
console.log(1); // > 1
console.log(2); // > 2
console.log(3); // > 3
```

何回実行しても、「1, 2, 3」の順番でコンソールに出力されるはずです。

この時「undefined」というメッセージも出力されるかもしれませんが、それは**今回関係ないので無視**しておきましょう。**数字の出力順序**だけに注目してください。

次のコードも試して見ましょう。

```javascript
var hoge = function () {
    console.log(2); // > 2
};

console.log(1); // > 1
hoge(); // コンソールに「2」と出力する関数の呼び出し
console.log(3); // > 3
```

こちらも先ほど同様、何回実行しても「1, 2, 3」の順番でコンソールに出力されます。

では次のコードはどうでしょうか？

```javascript
var hoge = function () {
    setTimeout(function () {
        // 100ミリ秒経った！
        console.log(2); // > 2
    }, 100);
};

console.log(1); // > 1
hoge(); // コンソールに「2」と出力する関数の呼び出し
console.log(3); // > 3
```

先ほどとは異なり、「1, 3, 2」という順に数字が出力されたのではないでしょうか？

このように呼び出した順番通りに処理が行われるものを「同期」、そうではないものを「非同期」と呼びます（※これはざっくり解釈です）。

例で使っている`setTimeout()`という関数は、`delay`（遅延処理）アクティビティと同じ役割を持ち、指定した時間分（今回は100ミリ秒）待ってから受け取った関数を実行するものです。

そういったケースでは、もともと`Inject JS Script`アクティビティの仕様にある「`return`文を利用してUiPathに結果を文字列で返す」という処理を実現することができません。`return`をするタイミングではまだ100ミリ秒経っていないためです。

単純に別々のJavaScriptに分けて実行タイミングを`delay`アクティビティで100ミリ秒まってあげれば良さそうですが、非同期処理の多くはこの例とは異なりいつ終わるかがはっきりしません。

JavaScriptによるファイルの取得などの終了タイミングはネットワーク環境に依存しますから、厳密に何秒待てばいいのかは誰にもわからないのです。

そこでUiPathでもJavaScriptの非同期処理が「どれくらいで終わるだろう」という予想による不安定な遅延ではなく、より安全に「非同期処理が終わっているのか」を知る方法が必要になってきます。


## UiPathでJavaScriptの非同期処理を待ってみよう

例として、開いたページの画像でリンク切れが発生していないかをフロントエンド側から検査するRPAを組んでみましょう。（サーバ側のエラーログ？なんですかそれは）

簡易的な例として、フローは次のようなものを想定します。

1. ページ内の全ての画像を表示している`img`要素を取得
2. １つ１つの`img`要素の`src`属性に書かれているパスへJavaScriptでアクセスしてみる
3. UiPath側に「読めない画像の有無」を伝えて制御を戻す

まずは、読み込めていない画像がいるかどうかを判定するコードを書いてみます。

※ 本記事では`Promise`や`fetch`に関する説明は割愛します。

```javascript

var iterable = []; // Array<Promise>

// 画像の数だけ fetch (非同期的に画像ファイルにアクセスする） する
document.querySelectorAll('img').forEach(function (img) {
    var src = img.src;

    iterable.push(fetch(src).then(function(res) {
        if (!res.ok) {
            throw new Error(); // 読めていなかった場合エラー判定にする
        }
    }));
});

Promise
    .all(iterable) // iterable の中の [object Promise]（fetchが終わるまで待機する約束） が全部終わっているか
    .then(function () {
        window.alert('全部読めた'); // 問題なく終わった
    })
    .catch(function () {
        window.alert('読めていない画像がある'); // 少なくとも1つ以上のエラーが吐かれた
    });

```

全ての画像が読めていれば、「全部読めた」と表示されます。

### 意図的に、画像をリンク切れさせて走らせてみる

インスペクタ（ブラウザの開発者ツールにあるDOMツリーを操作できるパネル）から、適当な`img`要素を探し出し、`src`属性の値を存在しないパスに書き換えて先述のコードを実行して見ましょう。

<img width="720" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/142709/d26cb8ae-1109-95a1-8c0c-45d7e3c8d2ad.png">

<img width="721" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/142709/901ea674-e7f8-7636-99f2-5d5579969e37.png">

このように、読めていない画像がページ内に存在していると「読めていない画像がある」とダイアログが出ると思います。

### UiPathが、JavaScriptの非同期処理の終了を検知できるように手がかりを作ってあげる

1. ~~ページ内の全ての画像を表示している`img`要素を取得~~
2. ~~１つ１つの`img`要素の`src`属性に書かれているパスへJavaScriptでアクセスしてみる~~
3. UiPath側に「読めない画像の有無」を伝えて制御を戻す

ここまでの間で３ステップのうち２つまでは解決できました。いよいよ非同期処理の完了をUiPathに伝えましょう。

UiPathには`Find Element`（要素を探す）アクティビティや`Get Text`（テキストを取得する）アクティビティなどの、Webページ内の要素を探すアクティビティが用意されています。

これまでの例のように、JavaScript側では「非同期処理が終わったタイミングで何かをする」というのが容易なので、非同期処理終了時に１つの要素を作り出して、DOMツリーに出力します。

その要素をUiPathに探させて、見つけることができたら、UiPathは非同期処理の終了を理解することができます。
先ほどのJavaScriptコードの「`window.alert`」の行を、要素の出力に書き換えるだけですね。

どちらも同じようなコードになりますから、一箇所に関数をまとめて書いておきます（`exportResult()`）。
前回同様、UiPathが`Inject JS Script`アクティビティで読み込めるように無名関数にしておきましょう。

```javascript
// 最終形態
function () {
    var iterable = []; // Array<Promise>
    var exportResult = function (message) {
        // input要素を作る。まだページには追加されてない
        var input = document.createElement('input');

        // UiPathのセレクタで見つけやすいIDを付与する
        input.id = 'RPA_VALUE';
        // 引数で受け取った文字列を値に入れる
        input.value = message;
        // 目で見てわかりやすいようにスタイリングする（任意）
        input.style.cssText = 'font-size: 20px; padding: 10px; position: relative; z-index: 9999; background: #fff; color: #333; box-sizing: border-box; width: 100%;';

        // ページの先頭に追加する
        document.body.prepend(input);
    };

    // 画像の数だけ fetch (非同期的に画像ファイルにアクセスする） する
    document.querySelectorAll('img').forEach(function (img) {
        var src = img.src;

        iterable.push(fetch(src).then(function(res) {
            if (!res.ok) {
                throw new Error(); // 読めていなかった場合エラー判定にする
            }
        }));
    });

    Promise
        .all(iterable) // iterable の中の [object Promise]（fetchが終わるまで待機する約束） が全部終わっているか
        .then(function () {
            exportResult('全部読めた'); // 問題なく終わった
        })
        .catch(function () {
            exportResult('読めていない画像がある'); // 少なくとも1つ以上のエラーが吐かれた
        });
    }
}

```

あとは、このJavaScriptを`Inject JS Script`アクティビティで実行後、次ようなセレクタを用いて、任意のアクティビティでUiPathにDOM探索させるだけです。

```xml
<webctrl tag='INPUT' id='RPA_VALUE' />
```

３ステップ目は『UiPath側に「読めない画像の有無」を伝えて制御を戻す』としていましたが、「伝えて」というより「結果を置いて読んでもらう」イメージですね。

このコードをカスタムしていけば、読めていない画像のパスをカンマ区切りの文字列にまとめて`input`要素に乗せて、UiPath側で拾ってから`split`してエクセルに投入してレポートを生成する、なんてこともできそうですね。

※ 例ではJavaScriptで`input`要素を作っていますが、コンテンツを持つことができる要素なら`id`属性さえ付与しておけばなんでもよさそうです（要素の種類によってコンテンツの入れ方は異なります）。


## 最後に

ここまでお付き合いいただきありがとうございます。

通常は、1つの関数内で`return`文で文字列を返せばそれで事足りるのですが、非同期処理が必要な場合に、要素を作り出してUiPathに見つけさせる方法は非常に有効です。

ただし、UiPathが要素を見つけるまでのタイムアウト時間が短いと意味がないですから、**見つけるまで待たせるように設定しておかなければならない**ことに注意が必要です（`TimeoutMS`を長時間に設定するか、`TryCatch`（トライキャッチ）アクティビティを利用して見つけられるまでループさせるなど）。

```javascript
// ポイントまとめ
var input = document.createElement('input'); // 要素を作る

input.id = 'RPA_VALUE'; // UiPathのセレクタで見つけやすいIDを付与する
input.value = '好きな文字列'; // 引数で受け取った文字列を値に入れる。div要素を作ったりした場合は innerText で値を入れる
input.style.cssText = 'font-size: 20px; padding: 10px; position: relative; z-index: 9999; background: #fff; color: #333; box-sizing: border-box; width: 100%;'; // 目で見てわかりやすいようにスタイリングする（任意）

document.body.prepend(input); // ページの先頭に追加する
```

画像やCSSなどのリンク切れを検証する方法は他にも方法があったりしますが、`Promise.all`と`XMLHttpRequest`の組みわせ方法や`fetch`との違いなどを解説するにはちょっとアドベントカレンダー畑が違いすぎると思い、コードも短くて済みますから、今回は`fetch`を採用しました。JavaScriptの基本がわかった上で、JavaScriptに興味がある方は`Promise`や`fetch`のベストプラクティスを調べてみてください。

本業がWeb屋なのでUiPathの活用方法がフロントエンド寄りな発想ばかりですが、またなにかご紹介できるものがあれば書こうかと思います。

12月2日の記事と合わせて読んでくださった方、重ねてありがとうございました。何かありましたら編集リクエストお待ちしております。