CLOVER🍀

That was when it all began.

ヘッドレスブラウザPhantomJSを試す

少し前にMochaをブラウザ上で動かしてみたのですが、この時はFirefoxを使っていました。

とはいえ、ブラウザを起動して確認するだけでなく、ヘッドレスブラウザというものも試してみたくて、PhantomJSを使ってみることにしました。

PhantomJS - Scriptable Headless Browser

WebKitを使っているそうです。

とりあえず、使ってみる

インストール

まずは、ダウンロードページからPhantomJSを取得します。

Download PhantomJS

今回のPhantomJSのバージョンは、2.1.1です。アーカイブを展開したら、パスを通しておきます。

$ wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
$ tar -jxvf phantomjs-2.1.1-linux-x86_64.tar.bz2
$ PATH=phantomjs-2.1.1-linux-x86_64/bin:${PATH}

確認。

$ phantomjs -v
2.1.1

Quick Startに沿って、いくつか試してみます。

Quick Start with PhantomJS

Hello World

最初は、Hello World。
hello.js

console.log("Hello World");
phantom.exit();

スクリプト終了時には、phantom.exitする必要があるようです。

実行。

$ phantomjs hello.js
Hello World

とりあえず、コンソール上で動いただけです。

Webページへのアクセスと、スクリーンショットの取得

Googleへアクセスしつつ、スクリーンショットを撮ってみます。
access-google.js

var page = require("webpage").create();

page.open("https://www.google.co.jp/", function(status) {
    console.log("status = " + status);

    if (status === "success") {
        page.render("google-top.png");
    }

    phantom.exit();
});

webpageをrequire、createでpageを取得し、

var page = require("webpage").create();

openでURLを指定してページを開きます。開いた後の動作を、コールバック関数に記述する、と。

page.open("https://www.google.co.jp/", function(status) {

スクリーンショットは、page.renderで取得できます。この例だと、カレントディレクトリに「google-top.png」というファイルが生成されます。

        page.render("google-top.png");

実行。

$ phantomjs access-google.js
status = success

取得したスクリーンショット。

少し、小さいですね。サイズを変更するには、pageにviewportSizeを設定します。

var page = require("webpage").create();
page.viewportSize = { width: 1024, height: 768 };

こちらを実行すると、もうちょっと大きなスクリーンショットになりました。

参考)
Screen Capture with PhantomJS

ネットワークログの取得

ほぼQuick Startのまんまですが、page.onResourceRequestedおよびonResourceReceivedで、リクエスト/レスポンス時のログが取得できます。
netlog.js

var page = require("webpage").create();

page.onResourceRequested = function(request) {
    console.log("Request: " + JSON.stringify(request, undefined, 4));
};

page.onResourceReceived = function(response) {
    console.log("Receive: " + JSON.stringify(response, undefined, 4));
};

page.open("https://www.google.co.jp/", function(status) {
    console.log("status = " + status);

    if (status === "success") {
        page.render("google-top.png");
    }

    phantom.exit();
});

実行。

$ phantomjs netlog.js 
Request: {
    "headers": [
        {
            "name": "Accept",
            "value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
        },
        {
            "name": "User-Agent",
            "value": "Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) PhantomJS/2.1.1 Safari/538.1"
        }
    ],
    "id": 1,
    "method": "GET",
    "time": "2016-03-14T14:07:35.830Z",
    "url": "https://www.google.co.jp/"
}
Receive: {
    "body": "",
    "bodySize": 17144,
    "contentType": "text/html; charset=UTF-8",
    "headers": [

〜省略〜

参考)
Network Monitoring with PhantomJS

とりあえず、簡単にPhantomJSを試してみました。

Mochaと組み合わせる

PhantomJS自体はテスト用のフレームワークではないため、他のテスト用のフレームワーク、Runnerと組み合わせる必要があります。

Headless Testing with PhantomJS

今回はMochaと組み合わせようと思ったのですが、mocha-phantomjsだと依存しているPhantomJSが1系で、2系とは事情が違いそうなのでパス。しかも、mocha-phantomjsの場合はPhantomJSからMochaを起動する形になるため、Mochaをテストで使うHTMLに含める必要があります。

微妙だなーと思って考えた結果、MochaからPhantomJSを起動してみることにしました。

PhantomJSは、npm moduleとしてインストールすることになります。

で、PhantomJSのnpm moduleなのですが、phantomjsはPhantomJS 2系では名前が変わったよ、と。

phantomjs - npm

「phantomjs-prebuilt」になったそうです。

phantomjs-prebuilt - npm

で、さらにこの「phantomjs-prebuilt」を包んだ「phantom」というモジュールがあったので、こちらを利用することにしました。

phantom - npm

プロジェクトの準備。

$ npm init
$ npm install -g mocha
$ npm install --save phantom should

作成したコードは、こちら。
test/example-test.js

var phantom = require("phantom");
var should = require("should");

describe("PhantomJS and Mocha Test", function() {
    it("Google Top, take screenshot", function() {
        var promise = phantom.create().then(function(ph) {
            return ph.createPage().then(function(page) {
                return page.open("https://www.google.co.jp/").then(function(status) {
                    status.should.be.equal("success");

                    var titlePromise = page.evaluate(function() {
                        return document.getElementsByTagName("title")[0];
                    }).then(function(title) {
                        title.textContent.should.be.equal("Google");
                    });

                    var submitPromise = page.evaluate(function() {
                        return document.getElementsByName("q")[0].value = "Cheese!";
                    }).then(function(q) {
                        return page.evaluate(function() {
                            return document.forms["f"].submit();
                        }).then(function() {
                            return new Promise(function(resolve, reject) {
                                setTimeout(function() {
                                    resolve();
                                }, 3000);
                            }).then(function() {
                                return page.evaluate(function() {
                                    return document.getElementsByTagName("title")[0];
                                }).then(function(title) {
                                    title.textContent.toLowerCase().should.containEql("cheese!");

                                    return page.render("google-search.png");
                                });
                            });
                        });
                    });

                    return Promise.all([titlePromise, submitPromise]).then(function() {
                        page.close();
                        ph.exit();
                    });
                });
            });
        });

        return promise;
    });
});

やっていることは、以前このブログでSeleniumでやったことと同じで

Selenium WebDriverで遊ぶ - CLOVER

Googleへアクセス、タイトル確認、Formに「Cheese!」と入力してSubmit、タイトル確認、という流れです。
追加で、最後にスクリーンショットを撮っています。

このphantomは、各メソッドがPromiseを返すようになっているので、Promiseをつなげていく処理をけっこうたくさん書いていくことになります。

Node.jsで初めて書いているので、このあたりのAPIには全然慣れていません…。

起点はphantom.create、page.openするところからで、Promiseが登場するところ以外は元のPhantomJSのイメージがあれば、なんとなく読める気がします…。

        var promise = phantom.create().then(function(ph) {
            return ph.createPage().then(function(page) {
                return page.open("https://www.google.co.jp/").then(function(status) {

page.evaluateのコールバック関数の中では、DOMを操作することができます。

                    var titlePromise = page.evaluate(function() {
                        return document.getElementsByTagName("title")[0];
                    }).then(function(title) {
                        title.textContent.should.be.equal("Google");
                    });

ブラウザ上でjQueryなどを読ませていれば、そのあたりも利用できるようです。Promise.thenでは、documentなどは触れませんが…。

その他、Submit後を確認するために「3秒待つ」みたいなことをするのにだいぶ苦労したり。今回はPromiseとsetTimeoutで書きました。

                            return new Promise(function(resolve, reject) {
                                setTimeout(function() {
                                    resolve();
                                }, 3000);
                            }).then(function() {
                                return page.evaluate(function() {
                                    return document.getElementsByTagName("title")[0];
                                }).then(function(title) {
                                    title.textContent.toLowerCase().should.containEql("cheese!");

                                    return page.render("google-search.png");
                                });
                            });

使ったpageとphantomのオブジェクトは、それぞれclose、exitする必要があります。

                        page.close();
                        ph.exit();

特にphantomオブジェクトは、破棄しないとPhantomJSのプロセスが残りっぱなしになります。

このコードだと、テストが失敗した時にPhantomJSのプロセスが残ったままになるので、そのあたりはちょっと微妙だったり…。before/afterでなんとかした方がいいでしょうか?

このテストはPromiseが最後の結果になりますが、MochaはPromiseをサポートしているようなので、そのままreturnしてあげればMocha側で結果を待ち合わせてくれるみたいです。

        return promise;

では、実行。このコードだと実行にけっこう時間がかかるので、タイムアウトを長めに設定。

$ mocha --timeout 60000


  PhantomJS and Mocha Test
    ✓ Google Top, take screenshot (12866ms)


  1 passing (13s)

スクリーンショットは、こちら。

とりあえず、少しは使えたのではないでしょうか?そのうち、gulpと合わせられれば…。