👋

続・さよなら WebDriver Client Agouti

2022/12/07に公開

前回までのあらすじ

Agouti は単純で分かりやすくてちょっとしたことに使いやすい Go の WebDriver Client ライブラリなのですが、もう積極的なメンテナンスはおこなわれないと知ってがっかりしていたのでした・・・。

https://zenn.dev/ikawaha/articles/20220722-4719183593a3cb

ということで、勉強がてらに移植して、整理して、供養して参ります。

Agouti

https://agouti.org/

Atouti の最初のコミットは2014年10月。Go のバージョンでいくと Go 1.3 の頃です。作者も Agouti は初期の Go プロジェクトで、API のデザインには満足していないとコメントしています。

Agouti は、chromedriver や geckodriver などの WebDriver サービスを立ち上げて、そのサービスの API を利用することでブラウザを操作します。WebDriver サービスへのアクセスは HTTP トランスポートを用いた JSONベースの REST API になっています。

WebDriver サービスへのアクセスの共通仕様は WebDriver -W3C Recommendation としてまとめられています。この仕様に沿って、chromedirver や geckodiriver が作られています。細かなオプションなどは異なるかも知れませんが、基本的には同じです。なので、Agouti は chromedriver や geckodriver など好みの WebDriver を選択して利用することができました。

Agouti のテストにはテストフレームワークの Ginkgo が利用されていて、目新しかったのを覚えています[1]

Clone して整理してみる

Agouti がやってることは

  • WebDriver サービスの起動
  • WebDriver サービスへの問い合わせ
    • ページ遷移
    • ページ内の要素の探査 など

と、やりたいことは至極単純なのですが、コードは若干読みにくい感じがします。これはテスト用にモックが用意されていて、モックを利用するために(?)インターフェースが切られていたりすることに要因があるのかなという印象を受けました。

インターフェースは処理を抽象化してくれて、融通が利く反面、具体的な処理の見通しはどうしても悪くなってしまいます。なので、思い切ってインターフェースは全部捨てて、具体的なコードだけを残すことにしてみました。

サービスの起動

WebDriver サービスの起動は難しいことはないです。os/exec ライブラリから、WebDriver のコマンドを起動するだけです。起動には多少時間がかかるので、Polling しながら待ちます。こういったよりは、Go 1.3 の頃にはなかった context.Context があるので、前よりもかなり簡潔に書けます。╭( ・ㅂ・)و ̑̑

サービスへの問い合わせ

サービスへの問い合わせは、通常の REST API へのアクセスなので、難しいことはないです。インターフェースで抽象化された部分を整理するのはかなり面倒でしたが、WebDriver の利用方法を把握していなくてもお手本となる元コードがあるので、単純に移植していきました。

ネットワーク越しのアクセスになるので、context.Context を渡すべきなのか?とか、ロギングするのをどうしたらいいのか?というのは相変わらず分かりませんでした。

さよなら Agouti 👋

ということで、Agouti の骨子の部分だけを持ってきた Navigator という WebDriver Client に移植しました。移植と言っても、まるっと持ってきただけなので、ほぼリファクタリングしただけです。Agouti では PhantomJS にも対応していたようですが、PhantomJS はもう開発が止まってしまっているようなので、サポートを落としました。(自分で使う分には、とりあえず chromedirver と geckodriver が動けばいいかな・・・。)

ブラウザを絡めた動作テストなどに Go から利用できます。使い方は Agouti と同じです。

テストが不足している部分がありますが、ボチボチ増やしていきたいです。

動作サンプル

準備

動作の確認のために httpbin.org の HTTP ページを利用させて貰います。手元の開発環境で動作可能なので、先に立ち上げておいてください。また、chromedriver を利用するのでインストールしておく必要があります。

docker run -p 80:80 kennethreitz/httpbin

たとえば chromedirver は MacOS で brew を利用している場合ならば brew install できます。

brew install chromedriver

実行

以下のコードを実行すると、Chrome ブラウザが立ち上がって、ページに遷移後、フィールドに値を埋めて Submit ボタンを押します。処理が終わるとブラウザはすぐ閉じてしまうので、必要なら適当に sleep を挟んでおくといいでしょう。

package main

import (
	"context"
	"fmt"
	"os"

	"github.com/ikawaha/navigator"
)

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func run() error {
	driver := navigator.ChromeDriver(navigator.Browser("chrome"), navigator.Debug)
	defer driver.Stop()

	ctx := context.Background()
	if err := driver.Start(ctx); err != nil {
		return fmt.Errorf("driver.Start() failed: %w", err)
	}

	page, err := driver.NewPage()
	if err != nil {
		return fmt.Errorf("dirver.NewPage() failed: %w", err)
	}

	page.Navigate("http://localhost:80/forms/post")

	if err := page.FindByName("custname").Fill("John"); err != nil {
		return err
	}
	if err := page.FindByName("custtel").Fill("0000000000"); err != nil {
		return err
	}
	if err := page.FindByName("custemail").Fill("example@example.com"); err != nil {
		return err
	}
	if err := page.All("fieldset").FindByLabel("Medium").Click(); err != nil {
		return err
	}
	if err := page.All("fieldset").FindByLabel("Bacon").Check(); err != nil {
		return err
	}
	if err := page.All("fieldset").FindByLabel("Onion").Check(); err != nil {
		return err
	}
	if err := page.FindByName("delivery").Fill("12:00"); err != nil {
		return err
	}

	if err := page.FindByButton("Submit order").Submit(); err != nil {
		return err
	}

	fmt.Println(page.Find("body").Text())

	// time.Sleep(10 * time.Second)

	return nil
}

こんな感じにフィールドを埋められます。

移植して満足したので、これで完全にお焚き上げ完了です。Agouti いままで本当にありがとう!

人のコードをまるっと読んだりするのは学びがありますね。

Happy hacking!

脚注
  1. この頃は特にテストに関する合意的なものはなかった気がします。今はテストにはライブラリを使わないでテーブルテストするのが主流でしょうか?Goa の v1 にも Ginko が使われていました。 ↩︎

Discussion