いまだにChromeに対応していないWebサービス、puppeteer-dartとdart2nativeで勝手にCLIを作ってやる！

## まえおき

世の中には、この令和の時代になってもなおChromeでは動かないWebサービスがあります。
勤怠システムや資産管理システムなど、業務ユーザしか使わないようなアプリケーションだと多いのではないでしょうか。

いまどき、**Chromeでまともに動かないWebサービスを提供するくらいなら、REST APIを提供してほしい**ものです。でも世の中そんなに甘くはありませんね。

そこで今回は、Chromeで動かなくてイライラするようなWebシステムを、dartを活用して勝手にCLIを作る方法を紹介します。

## ベースにする要素技術

### dart2native

DartはGoのようにシングルバイナリを作れます。

去年のアドベントカレンダーでサラッと書いてた記事があるので、それを見てください。
→ [dart2nativeで簡易的なコマンドラインツールを作る](https://qiita.com/YusukeIwaki/items/28c7dc837cf1a0f9407b)

### puppeteer-dart

これも去年のアドベントカレンダーで、dart2nativeを使ってググレカスするCLIアプリケーションの作り方を紹介していました。

→ [dart2nativeを使ってggrksコマンドを作る](https://qiita.com/YusukeIwaki/items/e09401743faff86567e1)

![image](https://user-images.githubusercontent.com/11763113/69257935-2dbabc00-0bff-11ea-8c83-3ff5006b302a.gif)

ぐぐれかすをするためには、ブラウザを動かす必要がありますが、その自動操作をするためのライブラリが [puppeteer-dart](https://github.com/xvrh/puppeteer-dart) です。（[puppeteer](https://pptr.dev/)をDartに移植された非公式のライブラリです）

文字入力、マウス操作はもちろんのこと、「特定のDOM要素が現れるまで待つ」 「画面遷移するまで待つ」 などができます。

### puppeteer-dart のFirefox対応版

ここからが今年の話です。

今年のテーマは Chromeでまともに動かない（けどFirefoxだとかろうじて動く） Webサービスに戦うことでした。
なんか条件が増えてるけど気にしないw

さて、puppeteer-dartは2020/12/05現在、Firefoxの自動操作には対応していません。

・・・じゃあどうする？

Firefox対応させるしかないですね。Firefox対応させました。
https://github.com/xvrh/puppeteer-dart/pull/125

ただ、まだマージされてないんで、pubspec.yamlにはGitのURLを指定する必要があります。

```pubspec.yaml
dependencies:
  puppeteer:
    git:
      url: git@github.com:YusukeIwaki/puppeteer-dart
      ref: feature/firefox
```


### Null safety "ではない" Dart SDK

2020年のDartの大きな変化といえば、なんといっても[Sound null safety](https://dart.dev/null-safety)でしょう。
コードを書くときは圧倒的に Null safetyが書きやすいです。

しかし！

dart2nativeはどうも**Null safetyじゃない依存ライブラリが１こでもあるとコンパイルが通らない**みたいなんです。

```
 $ dart2native main.dart -o awesome_app
Error: This project cannot run with sound null safety, because one or more project dependencies do not
support null safety:

 - package:cli_dialog
 - package:puppeteer
 - package:meta
 - package:async
 - package:pool
 - package:logging
 - package:archive
 - package:http
 - package:path
 - package:collection
 - package:dart_console
 - package:stack_trace
 - package:petitparser
 - package:crypto
 - package:http_parser
 - package:ffi
 - package:win32
 - package:convert
 - package:string_scanner
 - package:typed_data
 - package:source_span
 - package:charcode
 - package:term_glyph

Run 'pub outdated --mode=null-safety' to determine if versions of your
dependencies supporting null safety are available.


Failed to generate native files:
Generating AOT kernel dill failed!
```

`--no-sound-null-safety` オプションも今のところありませんし、いまのところ Dart SDK 2.10 (Null safety非対応）でdart2nativeするしか方法はなさそうです。

（実は方法があるよ！って知ってる人は教えて下さい）

### cli_dialog

CLI化する場合には、いまのところ [cli_dialog](https://pub.dev/packages/cli_dialog)がシンプルかなと思います。

![cli_dialog](https://github.com/leonardoInf/cli_dialog/raw/master/doc/cli_dialog_demo.gif)

* テキストで入力をもらう
* 選択肢から選んでもらう
* yes/no を回答してもらう

が、簡単にできます。

DartはGoにくらべると、コンソールアプリケーションの便利ライブラリは少なくて、
cli_dialogも、正直かゆくて手が届かないところが多いです。OSSコントリビューションチャンスがいっぱいですね（白目）


## 組み合わせる

### まず自動操作側を作ってしまう

```dart
class KusoServiceTop {
  Page page;
  KusoServiceTop(this.page);

  browseToLoginPage() async {
    await page.click("#sub_menu");

    await Future.wait([
      page.waitForNavigation(),
      page.click("li.login_page"),
    ]);
  }
}

class KusoServiceLogin {
  Page page;
  KusoServiceLogin(this.page);

  submitCredential(String username, String password) async {
    await page.waitForSelector("input[name='username']");
    await page.click("input[name='username']");

    await page.keyboard.type(username);
    await page.keyboard.press(Key.tab);
    await page.keyboard.type(password);
    await page.keyboard.press(Key.tab);

    await Future.wait([
      page.waitForNavigation(),
      page.keyboard.press(Key.enter),
    ]);
  }
}

class KusoServiceDataList {
  Page page;
  KusoServiceDataList(this.page);

  list() async {
    final cells = await page.$$(".cell_item");

    // 省略
  }
}
```

きれいなページオブジェクトになっていなくても、とりあえず画面単位でクラスを分けるとよいです。

メインアプリケーション側のコードは、一旦CLIのプロンプト操作などを一切含まず、上から下に流れるだけのものにします。

```main.dart
import 'package:puppeteer/puppeteer.dart';

doWithBrowser(Browser browser) async {
  final page = await browser.newPage();
  await page.setViewport(DeviceViewport());

  await page.goto("http://kusoservice.example.com/");

  final top = KusoServiceTop(page);
  await top.browseToLoginPage()

  final login = KusoServiceTop(page);
  await login.submitCredential("YusukeIwaki", "secretsecret");

  final dataList = KusoServiceDataList(page);
  for(DataItem item in await dataList.list()) {
    print(item);
  }
}

main() async {
  final browser = await puppeteerFirefox.launch();

  try {
    await doWithBrowser(browser);
  } finally {
    await browser.close();
  }
}
```

### CLI部分を作り込む

#### ID/パスワードを問う

cli_dialogのサンプルほぼそのままですが、以下のように書けば `ask()` のところで処理がブロッキングされて、ユーザ入力を受け付けます。ユーザ入力が済むと、処理が再開され、ログイン操作が行われます。

```dart

  final loginDialog = CLI_Dialog(questions: [
    ["User Name:", "username"],
    ["Password:", "password"],
  ]);
  final loginDialogAnswer = loginDialog.ask();

  await login.submitCredential(
    loginDialogAnswer['username'],
    loginDialogAnswer['password'],
  );
```

ちなみに、少し話は逸れますが、Goの[viper](https://github.com/spf13/viper)みたいなライブラリは、今のところDartにそんな便利なライブラリはありません。

#### データを取得してきたものから、1つを選択してもらう

```dart
  // itemListが選択肢

  while (true) {
    final selectDataDialog = CLI_Dialog(listQuestions: [
      [
        {
          'question': 'Select data',
          'options':
              ["Exit"] + itemList.map((item) => item.toString()).toList(),
        },
        'data'
      ],
    ]);
    final String selectDataAnswer = selectDateDialog.ask()['data'];
    if (selectDataAnswer == 'Exit') break;

    // selectDataAnswerが選択されたもの
  }
```

cli_dialog の仕様上どうしても、選択肢は文字列のリストで、選択されたものも選択肢の文字列そのままで返されてしまいます。

たとえば

```
 12/1: xxxx
 12/2: yyyy
 12/3: zzzz
 12/4: aaaa
 12/7: bbbb
```

のような選択肢を出すには

`["12/1: xxxx", "12/2: yyyy", "12/3: zzzz", ..... ] ` を渡して、かりに12/7が選ばれると `"12/7: bbbb"` が文字列として返ってくる、という感じです。

仕様としては非常にシンプルではありますが、「文字列」であることがとてもネックで、
たとえば勤怠データとかだと、「勤怠データから文字列への変換」「文字列から勤怠データへの変換」を用意しないといけません。

このデータのシリアライズ/デシリアライズ処理が増えるのが結構めんどくさいです。
HTMLのselect/optionみたく、表示テキストとvalueが別に指定できればいいんですけどね・・・。

あとは、DartはPythonのようにCtrl+Cをカジュアルに処理しづらいので、選択ダイアログをを抜けるための選択肢（上のサンプルだとExit）を１つ追加しておいたほうが無難です。


## 完成

（私が作ったのは会社の勤怠システム用のCLIなので、残念ながらスクショ貼れません・・・ :cry: 　そのうち気が向いたらサンプル作って貼ります）

　


今回は cli_dialogを活用したCLIを作る話でしたが、Dart標準の[HttpServer](https://dart.dev/tutorials/server/httpserver)を使えばJSON API化することもできるでしょう。

## まとめ

令和にもなってChromeに対応していないWebサービスは、おそらく今後もずっとChromeには対応しないでしょう。
そんなWebサービスをわざわざFirefoxに切り替えて使わないといけないストレスから開放されるための、CLIツールの作り方を紹介してみました。（本当の敵はIEじゃないと倒せないかもしれませんがｗ）

Dartはシングルバイナリにビルドできるし、Goに比べるとゆるふわなコマンドラインアプリケーションを作るのに向いています。

みなさんの周りに、見たくもないWebサービスがもしもあれば、Dartの勉強がてらCLIを作ってみてはいかがでしょうか。