Goで書かれたStatefulなVirtual Web BrowserライブラリのSurfでJavaScriptが処理できるようにしてみたかった

# はじめに
これは [Go3 Advent Calender](https://qiita.com/advent-calendar/2018/go3)の22日の記事です。
大分遅れて申し訳ありません…。

**TL; DR;**
出来る限り頑張りましたがottoとwebloopでは実現することが出来ませんでした…。orz
今後ottoとwebloop以外の方法もトライしてみたいと思います。

今回Advent Calender駆動開発で、以前作った[Slackにemojiをアップロードするツール](https://qiita.com/from_Unknown/items/c6d0f16fe208d382404b)を最新化しようかと考えていましたが、いきなり壁にぶち当たりました。
上記のツールはSlackにemojiをアップロードするAPIがないため、[Surf](https://github.com/headzoo/surf)という仮想ブラウザのライブラリを使って、半ば無理やりemojiをアップロードしていたのですが、SlackのUIがアップデートされてから動かなくなっていたのです。
Slackのemojiをアップロードするまでの遷移が

- ページ内のフォームに入力

から

- 「絵文字を追加する」ボタンをクリック
- モーダルダイアログ内のフォームに入力

という遷移に変わったため、それに合わせてプログラムを修正する必要がありました。
最初は単純に考えていて、Surfの `brower.Click()` でダイアログを出して入力するだけでよいと思っていました。

しかしそう単純ではなかったのです…。
[SurfのClick()はJavaScriptのイベントの発火はサポートしていなかった](https://github.com/headzoo/surf/blob/master/browser/browser.go#L336)のです…。
実際Surfのbrowserのソースを見るとhttpのRequestやResponse、Cookie、DOM用にgoqueryなど、JavaScript関連の処理は行っていない様でした。

これは困りました。これが出来ないとそもそもやりたいことが出来ません。
という訳でSurfにどうやったらJavaScriptを実行する機能が付けられるか挑戦してみました。

## まずは調査
GolangでJavaScriptを実行出来る様なヘッドレスブラウザがないかどうか調べていたところ、[goqueryのTips and tricksにHandle Javascript-based Pages](https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks)という項目がありました。
ここでは以下の2つのライブラリが紹介されていました。

- JavaScriptパーサー [otto](https://github.com/robertkrimen/otto)
- ヘッドレスブラウザ [webloop](https://github.com/sourcegraph/webloop)

この2つでどうにか出来ないかやってみました。

### otto
こちらはJavaScriptを実行して結果を取得できるというものですが、全てGolangで書かれているというものすごいライブラリでした。
これでページ内のJavaScriptを実行すれば出来るはず…と思って調べたのですが、こちらあくまでもjavaScriptを実行するためのライブラリとの事で、[HTMLを読み込んでDOMを操作するといったことは出来ない](https://github.com/robertkrimen/otto/issues/18)とのことでした。
ですが、イシュー等を見た限り、[Reactを動かしたり](https://github.com/robertkrimen/otto/issues/67)、[ページ内の一部のJavaScriptを動かして必要な情報を取得したり](https://gist.github.com/cryptix/87127f76a94183747b53)といった使い方も出来るらしいです。

上記の事から今回はottoではHTMLを渡してJavaScriptを実行することは難しいと判断しました。

### webloop
こちらはGolangからwebkit2を動かすgo-webkit2を使っているヘッドレスブラウザだそうで、URLを読み込んでJSを実行したり出来るそうなので、これならやりたい事が出来るのでは、と思いました。

方法としては、webloopはURIからソースを読み込むだけではなく、HTMLをstringにして読み込ませることが出来る様なので、Surfから必要な時にHTMLを取り出してwebloopでJavaScriptを処理して結果をSurfから返すという流れを考えました。

#### 環境のセットアップ
今回使ったのは(ちょっと古いのですが)macOS Sierraです。

[go-webkit2](https://github.com/sourcegraph/go-webkit2)のページを見て必要な依存関係をインストールします。
[こちらのイシュー](https://github.com/sourcegraph/webloop/issues/3)を見て、Macの場合以下のコマンドを発行すれば良いことが分かりました。

```sh
$ sudo port install webkit2-gtk
$ sudo port install webkit-gtk3
```

しかし、ずっと前にMacPortsをインストールしたまま放置していた私は既に入っていたものが古かったのか、中々ビルドが成功しませんでした。
結局以下のコマンドで一度全部消してインストールし直しました。

```shell
$ sudo port -fp uninstall installed
```

途中依存関係のあるもののinstallに失敗した場合は、一度失敗したものだけcleanしてinstallしたら成功しました。

```shell
sudo port clean 失敗したもの
sudo port install 失敗したもの
```

#### テストしてみる
エラーが解消されたら、まずはgo-webkit2のテストをいくつか動かしてみました。
しかしなんだか動きが怪しい…。
VS Codeでテストを実行すると失敗したり、処理が帰って来なかったりと動かない。
ステップ実行すると成功したりと、もう既に不安しかない。

そしてwebloopの方のテストはデバッグ実行でもうまく動かない…。
webloopは諦め、go-webkit2で頑張ることにしました。

既存のテストにないファイルから読み込んでテストするパターンを試してみます。

```test_simple.html
<!doctype html>
<html>
	<head>
		<title>test</title>
	</head>
	<body>
		<button id="button1">test</button>
		<div id="target"></div>
		<script>
			document.getElementById('button1').addEventListener('click', function() {
				document.getElementById('target').innerHTML = '<p>good</p>';	
  			});
		</script>
	</body>
</html>
```

```simple_test.go
func TestWebView_RunSimpleJS(t *testing.T) {
	webView := NewWebView()
	defer webView.Destroy()

	loadOk := false
	webView.Connect("load-failed", func() {
		t.Errorf("load failed")
	})
	webView.Connect("load-changed", func(_ *glib.Object, loadEvent LoadEvent) {
		switch loadEvent {
		case LoadFinished:
			webView.RunJavaScript(`document.getElementById("button1").click()`, func(result *gojs.Value, err error) {
				if err != nil {
					t.Errorf("RunJavaScript error: %s", err)
				}
				webView.RunJavaScript(`document.getElementsByTagName("body")[0].outerHTML`, func(result *gojs.Value, err error) {
					resultString := webView.JavaScriptGlobalContext().ToStringOrDie(result)
					fmt.Println(resultString)
					if strings.Count(resultString, "<p>good</p>") > 1 {
						loadOk = true
					}
					gtk.MainQuit()
				})
			})
		}
	})

	f, err := os.OpenFile("test_simple.html", os.O_RDONLY, 0755)
	if err != nil {
		t.Errorf("File open err: %s", err)
	}
	defer f.Close()

	scanner := bufio.NewScanner(f)

	htmlData := ""
	for scanner.Scan() {
		htmlData = htmlData + scanner.Text() + "\n"
	}

	glib.IdleAdd(func() bool {
		webView.LoadHTML(htmlData, "")
		return false
	})

	gtk.Main()

	if !loadOk {
		t.Error("!loadOk")
	}
}
```

結果無事にステップ実行しなくても成功しました。

```
API server listening at: 127.0.0.1:16583
dbus[55590]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
dbus[55594]: Dynamic session lookup supported but failed: launchd did not provide a socket path, verify that org.freedesktop.dbus-session.plist is loaded!
<body>
		<button id="button1">test</button>
		<div id="target"><p>good</p></div>
		<script>
			document.getElementById('button1').addEventListener('click', function() {
				document.getElementById('target').innerHTML = '<p>good</p>';	
  			});
		</script>
	

</body>
PASS
```

#### 本命のHTMLを突っ込んで見る
さて、簡単なHTMLが動作したので本命のページから抜いてきたHTMLを突っ込んでJavaScriptを実行してみたのですが…。
結果は駄目でした。
JavaScriptを実行する箇所が待っても返ってきません。

まだ調査の余地はあるのですが、時間も大分過ぎてしまっているので今回はここまでとすることにしました :bow: 

#### やってみて
そもそもscriptタグ内で指定された外部のスクリプトは取得されているのかとか、普通に見る場合は認証が必要なページだが大丈夫なのだろうかなど、色々と検証が不足しているのですが、Webloopがちゃんと動くのかどうかが知りたいところです。
また、認証情報ごと渡してJavaScriptを動かす必要があるページだけ動作させられれば良さそうですが、その辺りも調べてみたいです。
他にもいくつかGolangで動作するHeadless Browserはある様なので、そちらが使えそうかどうか見てみたいと思いました。

## P. S.
[このイシュー](https://github.com/slackhq/slack-api-docs/issues/28)の今年の9月辺りの書き込みを見たところ、ドキュメントには載ってないけど `/api/emoji.add` にユーザートークンとmultipart formアップロードすればemojiが追加できると書かれていました。
いやーAPIが追加されていないか、もっと早い段階で念のため調べておくべきでした…。orz
このAPIを使用したプログラムに書き換えれば、取り敢えずやりたい事は出来そうです。
皆さんも時間が経ったらまずは欲しいAPIが提供されてないか確認しましょう。