WebDriver+JavaScriptでWebアプリのE2Eテスト

Webアプリが正しく動作することを、Webブラウザーを操作して確認する E2E (End-to-End) テスト。テストの記述には様々なプログラミング言語が使えます。

Selenium WebDriver + JavaScript で E2E テストをするやり方が

An Introduction to WebDriver Using the JavaScript Bindings – Tuts+ Code Tutorial

で紹介されています。

この記事は、基本となる WebDriverJS 以外に、7つのクライアントAPIライブラリーを紹介しています。どれも github で公開されていたので、スター数を調べてみました (2014/12/07時点)。

また、テストの際に、期待通りの値になっているかどうかを調べる assert API がライブラリに組み込まれているか、それとも別途 jasmine や mocha, Q といったテストフレームワークが必要かも調べました。

Client API スター数 assert 補足
WebDriverJS N/A 別途 W3Cで標準化しているAPIで書く。JavaでSelenium動かしていた人向け。
WD.js 658 別途 builder pattern (fluent interface) で書く。
WebDriver.io 661 別途 builder patternで書く。WD.jsより短く書ける。
Testium 203 別途 CoffeeScript で書く。
Leadfoot 63 別途 Internで使われてる。WD.jsと似たAPI。
Nightwatch 2,386 組み込み 設定ファイルが必要。拡張コマンドは別ファイルに書く。
DalekJS 481 組み込み Selenium Server まで入った全部入り。Webサイトが派手w。
Webdriver-sync 36 組み込み Java APIに準拠。同期型。

さらに、WebdriverJS, WD.js, WebdriverIO, Nightwatch, Dalek の5つについて、実際にコードを書いてみました。

お題は「Googleで”webdriver”を検索した時のヒット数を標準出力に表示する」です。assert は使いません。

共通の事前準備

Mac を使います。Web ブラウザーは Chrome です。
Homebrew と Cask で node.js と java をインストールします。

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
$ brew install node
$ brew brew install brew-cask
$ brew cask install java

さらに Selenium Server と Chrome Driver をインストールします。

$ npm install selenium-standalone chromedriver -g

コードを実行する前に、Selenium Server を起動しておきます。ただし、Dalek の場合は不要です。

$ start-selenium

WebDriverJS

APIはこちら。豊富です。
http://selenium.googlecode.com/git/docs/api/javascript/index.html

インストール

$ npm install selenium-webdriver

コード

webdriverjs.js:

var webdriver = require('selenium-webdriver');
var By = webdriver.By;

//WebブラウザーはChrome
var driver = new webdriver.Builder().
   withCapabilities(webdriver.Capabilities.chrome()).
   build();
var $ = driver.findElement.bind(driver);

//Googleを開く。
driver.get('http://www.google.com');

//検索ボックスにwebdriverと入力する。
$(By.name('q')).sendKeys('webdriver');

//検索ボタンを押す。
$(By.name('btnG')).click();

//ヒット数が表示されるまで待つ。
var timeoutMSec = 2000;
driver.wait(webdriver.until.elementLocated(By.id('resultStats')), timeoutMSec)
.then(function() { //waitした後はthenでつなぐ
    $(By.id('resultStats')).getText().then(function(text) {
        console.log(text);
    });
})
.then(function() {
    driver.quit();
});

実行

$ node webdriverjs.js 
約 615,000 件 (0.11 秒) 

WD.js

sendKeys() など WebDriverJS の名残があります。また、waitForElementByCss()など、APIが若干長めです。

インストール

$ npm install wd

コード

wd.js:

var wd = require('wd');
var browser = wd.promiseChainRemote();
var timeoutMSec = 1000;
browser
    .init({
        browserName: 'chrome'
    })
    .get('http://www.google.com')
    .elementByName('q')
    .sendKeys('webdriver')
    .elementByName('btnG')
    .click()
    .waitForElementByCss('#resultStats', timeoutMSec)
    .text(function(err, text) {
        console.log(text);
    })
    .quit();

実行

$ node wd.js
約 615,000 件 (0.12 秒)

Webdriver.io

今回試した中では、API 名が短くて好きです。

インストール

$ npm install webdriverio

コード

webdriverio.js:

var timeoutMSec = 1000;
var webdriverio = require('webdriverio')
    .remote({
        desiredCapabilities: {
            browserName: 'chrome'
        }
    })
    .init()
    .url('http://www.google.com')
    .setValue('[name="q"]', 'webdriver')
    .click('[name="btnG"]')
    .waitFor('#resultStats', timeoutMSec)
    .getText('#resultStats', function(err, text) {
        console.log(text);
    })
    .end();

実行

$ node webdriverio.js 
約 615,000 件 (0.12 秒) 

Nightwatch

runner を使って実行するので、出力が綺麗です。
デフォルトのブラウザーが Firefox で、Chrome を使うためには設定ファイルが必要です。
拡張コマンドを置くためのディレクトリが必要です。

インストール

$ npm install nightwatch -g
$ mkdir -p examples/custom-commands

コード

nightwatch.json:

{
  "selenium": {
    "cli_args": {
      "webdriver.chrome.driver": "/usr/local/bin/chromedriver"
    }
  },
  "test_settings" : {
    "default" : {
      "silent": true,
      "desiredCapabilities": {
        "browserName": "chrome"
      }
    }
  }
}

nightwatch.js:

module.exports = {
  "webdriverの検索ヒット数を表示する" : function (browser) {
    var timeoutMSec = 1000;
    browser
      .url("http://www.google.com")
      .setValue('[name="q"]', 'webdriver')
      .click('[name="btnK"]') // FirefoxとChromeでは表示されるボタン名が違います。
      .waitForElementPresent('#resultStats', timeoutMSec)
      .getText('#resultStats', function(res) {
          console.log(res.value);
      })
      .end();
  }
};

実行

$ nightwatch -t nightwatch.js 

[Nightwatch] Test Suite
=======================

Running:  webdriverの検索ヒット数を表示する 

✔  Element <#resultStats> was present after 1038 milliseconds.
約 615,000 件 (0.14 秒) 

OK. 1 total assertions passed. (11.812s)

DalekJS

Selenium server を動かす必要はありません。

インストール

$npm install dalek-cli -g
$npm install dalekjs dalek-browser-chrome --save-dev

コード

waitForElementが動作しないので、waitForを使いました(github)。
標準出力への表示はexecute()とlog.message()を組み合わせて使います。

dalek.js:

module.exports = {
  'webdriverのヒット数を表示する': function (test) {
    var timeoutMSec = 10000;
    test
      .open("http://www.google.com")
      .setValue('[name="q"]', 'webdriver')
      .click('[name="btnG"]')
      //.waitForElement('#resultStats', timeoutMSec)
      .waitFor(function() {
        return Boolean(document.querySelector('#resultStats'));
      }, [], timeoutMSec)
      .execute(function() {
        var result = document.querySelector('#resultStats').innerText;
        this.data('result', result);
      })
      .log.message(function() {
        return test.data('result');
      })
      .done();
  }
};

実行

引数 -b chrome を与えて Chrome で動かします。
デフォルトではヘッドレスブラウザーの PhantomJS で動かします。

$ dalek dalek.js -b chrome
Running tests
Running Browser: Google Chrome
OS: Mac OS X 10.10.1 x86_64
Browser Version: 39.0.2171.71

RUNNING TEST - "webdriverのヒット数を表示する"
▶ OPEN http://www.google.com
▶ SETVALUE [name="q"]
▶ CLICK [name="btnG"]
▶ WAITFOR 
▶ EXECUTE 
☁ [USER] MESSAGE: 約 615,000 件 (0.38 秒) 
✔ 0 Assertions run
✔ TEST - "webdriverのヒット数を表示する" SUCCEEDED

 0/0 assertions passed. Elapsed Time: 4.44 sec 

どれを選ぶか

短い API が好きなので、Webdriver.ioNightwatchですね。すでにテストフレームワークをチームに導入しているかどうかで決める。

(ただ、Nightwatch の対抗馬としてInternも気になります。もうちょっと調べてみよう)

ブックマークレットをChrome拡張機能から実行する方法

ブックマークレットは便利だけれど、インストールが大変。そこで、Chrome拡張機能として作り直してみた。結果的には、2つのファイルを追加すれば実現できた。

「ボタンを押すとスクリプトを実行する」という動作にしたかったので、Page Action として作った。スクリプトを動作させたいページを開くと、アドレスバーの横にアイコンを表示する。そのアイコンをクリックすると、スクリプトを実行する。

(1) manifest.json を作る
ポイントは、permissions に、”tabs”と、スクリプトを注入するページのURLを記載する点。
URLはアスタリスク (*) を使ったパターンを記述することもできる。詳しくは Match Patterns を参照。


{
"manifest_version": 2,
"name": "Name of this extension",
"version": "0.1", // 新しい拡張を公開するたびに上げていく
"description": "Description of this extension",
"icons": {
"128": "icon128.png" // chrome://extensions に表示されるアイコン。128×128
},
"page_action": {
"default_icon": "icon128.png", // アドレスバーに表示されるアイコン。19×19, 38×38 を準備しておくべき?
"default_title": "Tooltip for the page action icon"
},
"permissions": [
"tabs",
"https://www.google.com/maps/preview" // スクリプトを実行したいURLを記載していく
],
"background": {
"scripts": ["background.js"]
}
}

view raw

manifest.json

hosted with ❤ by GitHub

(2) background.js を作る
ポイントは2つ。
1. Page Action のアイコンを表示したいURLの正規表現を記述する。正規表現と、上記 (1) のアスタリスク記法は異なるので注意。
2. 実行したいスクリプト (ブックマークレット) のファイル名、ここでは execute.js を記述する。別ファイルにせずに、直接記述することもできる (file ではなく code にする)。


var urlRegExp = new RegExp('^https?://www.google.com/maps/preview'); // アドレスバーの横にアイコンを表示するときのURL正規表現
function showIcon(tabId, changeInfo, tab) {
if (tab.url.match(urlRegExp)) {
chrome.pageAction.show(tabId);
}
}
chrome.tabs.onUpdated.addListener(showIcon);
chrome.pageAction.onClicked.addListener(function(tab) {
chrome.tabs.executeScript(tab.id, {
"file": "execute.js" //実行したいスクリプトのファイル名 (execute.js)
}, function () {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError.message);
}
});
});

view raw

background.js

hosted with ❤ by GitHub

ここまで準備ができれば、Chrome単体で拡張機能を作って試すことができる。ただ、簡単にインストールできるようにするためには、Chromeウェブストアで公開する必要がある。

(3) Chromeウェブストアで公開する
まだ公開していないので、できたら報告します。
ダッシュボードにアクセスして、拡張機能を登録。ポイントは2つ。
1. 開発者登録に $5 必要。一度払えば、ずっと使えるらしい。
2. ローカルのChromeで作った.crxではなく、素の zip で固めた .zip ファイルを登録する。Chromeウェブストア側で秘密鍵を発行して署名してくれるらしい。

参考:

jsdo.itでenchant.jsを動かす

enchant.jsを使ったゲーム作り実習をやったので、その時のメモを公開。
jsdo.it で作ると、スマフォで動かすのが楽。
ちなみに私はスマフォ用に「シュシュっと手裏剣」を作りました。

(1) jsdo.it でアカウントを作る
http://jsdo.it/ で好きなサービスを使ってログイン。アカウント名はログイン後に決める。

(2) Hello World
画面に文字を表示しよう! by shi3z – code.9leap.net
http://code.9leap.net/codes/show/203
をコピペして動かしてみる。

手順:
1. Start coding を押下。
2. JavaScript タブを選択。
3. + Add Library を押下。
Major Library から enchant.js v0.6.2 – js を選択し、Add ボタンを押下。
4. 9leapのサンプルから main.js をコピペ。
5. Save ボタンを押下。実行結果を確認。

もし正しく動作しない場合は、JavaScript コンソールを確認。

(3) Start 画面追加
jsdo.it 上の別コードをインポートして、Start 画面を表示してみる。

手順:
1. JavaScript タブを選択。
2. + Add Library を押下。
Input URL に 9leap / nineleap.enchant を入力し、Add ボタンを押下。
3. ソースコードを修正。
// game = new Game();
game = new enchant.nineleap.Game();

(4) 画像を表示
スプライトを表示しよう by shi3z – code.9leap.net
http://code.9leap.net/codes/show/202
をコピペ+修正して動かしてみる。画像ファイルのURLを指定する必要がある。
使える素材は
Image Materials | enchant.js – A simple JavaScript framework for creating games and apps.
http://enchantjs.com/ja/image-materials/
を参照。商用利用でなければ、画像を改変して使うのも自由 (by @enchantjs_jaさん)。

手順:
1.
// var game = new Game(320, 320);
var game = new enchant.nineleap.Game(320, 320);

2.
// game.preload(‘chara1.gif’); // chara1.gifを読み込む
var chara1 = ‘http://enchantjs.com/images/materials/chara1.png’;
game.preload(chara1); // chara1.gifを読み込む

3.
// bear.image = game.assets[‘chara1.gif’]; // chara1.gifの中にある
bear.image = game.assets[chara1]; // chara1.gifの中にある

(5) 操作パッド
十字キーは Pad, アナログの操作キーは APad。
ソースコードは http://jsdo.it/takatama/Tn9F/ を参照のこと。

手順:
1. JavaScript タブを選択。
2. + Add Library を押下。
Input URL に 9leap / ui.enchant と入力し Add ボタンを押下。
3. ソースコードを入力。

var pad = new enchant.ui.Pad();
pad.moveTo(10, 220);
game.rootScene.addChild(pad);

bear.addEventListener('enterframe', function () {
    var input = game.input;
    //キーに応じて移動
    if (input.left) {
        this.x -= 4;
    }
    if (input.right) {
        this.x += 4;
    }
    if (input.up) {
        this.y -= 4;
    }
    if (input.down) {
        this.y += 4;
    }
    if (game.frame %4 === 0) {
        this.frame = 2- this.frame;
    }
});

var apad = new enchant.ui.APad();
apad.moveTo(120, 220);
game.rootScene.addChild(apad);

bear.addEventListener('enterframe', function () {
    if (apad.isTouched) {
        this.x += apad.vx*4;
        this.y += apad.vy*4;
    }
});

(6) スマフォで操作する
URLを jsdo.it から jsrun.it に変更して、スマフォのブラウザーからアクセス。
例えば http://jsdo.it/takatama/Tn9F/ なら http://jsrun.it/takatama/Tn9F/ にすればよい。

(7) End画面を追加
上記(3)でStart画面を追加済みなら、ゲーム終了時に、game.end(score, message); を呼び出せば良い。

(8) Clear画面を追加
ちょっとした工夫が必要。Gameをnewする前後にソースコードを追記する。

enchant.nineleap.assets.push('http://wise9.github.com/enchant.js/images/clear.png');
var game = new enchant.nineleap.Game();
game.clear = function(score, message) {
    this.endScene = new SplashScene();
    this.endScene.image = this.assets['http://wise9.github.com/enchant.js/images/clear.png'];
    this.end(score, message);
};

Google Analyticsを使ったコホート分析で施策の有効性を検証する

サービスを成長させるためには、機能追加や削除、デザイン変更、広告など、様々な施策によって新たなユーザーを集客する必要がある。ある施策によって集客したユーザーの集団(コホート)の動向を時系列で分析する「コホート分析」により、その施策の有効性を検証することができる。

例えば、獲得ユーザーを月ごとに分け、再訪問者数をプロットしている(How to do Cohort Analysis in Google Analyticsより引用)。獲得した次の月になると、再訪問者は大幅に下がる様子がわかる。9月と11月の獲得ユーザーで比べると、11月の方が月を再訪問者数を保持できており、改善効果が見て取れる。

以下では、Google Analyticsを使い「初訪問した時期」でコホート分析するための具体的な手続きを紹介する。

(1) トラッキングコードの設置

Google Analyticsのトラッキングコードが以下だとして、

<script type="mce-text/javascript">
    var _gaq = _gaq || [];
    _gaq.push(['_setAccount', 'UA-XXXXXXXX-X']);
    _gaq.push(['_trackPageview']);
    (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
     })();
</script>

次のようなコードを追加して、ユーザーの初訪問日をyyyymmdd形式でカスタム変数に保存する。具体的には、2013年3月28日に初訪問していたユーザーは、カスタム変数名「FirstVisit」に、値「20130328」を保存する。

<script type="mce-text/javascript">
    var _gaq = _gaq || [];

    try {
        var f = function (d) {
            return d.getFullYear() + ("0" + (d.getMonth() + 1)).slice(-2) + ("0" + d.getDate()).slice(-2);
        };
        var c = document.cookie;
        var i = c.indexOf('__utma');
        var s = null;
        if (i < 0) {
            s = f(new Date());
        } else {
            var utma = c.substring(i, c.indexOf(';', i));
            if (utma) {
                var v = utma.split('.');
                if (v[2]) {
		    var date = new Date();
                    date.setTime(v[2] * 1000);
                    s = f(date);
                }
            }
        }
        if(s) {
            _gaq.push(['_setCustomVar', 1, 'FirstVisit', s, 1]);
        }
    } catch (e) {
    }

    _gaq.push(['_setAccount', 'UA-XXXXXXXX-X']);
    _gaq.push(['_trackPageview']);
    (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
     })();
</script>

trackPageviewを実行するより前に、setCustomVarを実行する点に注意する(Recommended Practiceを参照のこと)。

(2) アドバンスセグメントを使った分析

Google Analyticsの左側ペインで、ユーザー>カスタム>カスタム変数、を選択し、カスタム変数 FirstVisit が取得できていることを確認する。もし、取得できていない場合は、Google Analyticsで閲覧している日付に間違いがないか、埋め込んだトラッキングコードに間違いがないか(正しく実行できていれば、__utmvというクッキーが作成されている)などを見直す。

スクリーンショット 2013-03-28 17.00.43

以下、「2013年3月に初訪問した人」の集団について分析するため、「FirstVisit の値が 201303 になっている人」を抽出するカスタムセグメントを作る。

右側ペインの上部の「アドバンスセグメント」を選択し、右下の「新しいカスタムセグメント」を押下する。

スクリーンショット 2013-03-30 1.49.18

下図のように「カスタム変数(キー1)」は「FirstVisit」と「完全一致」、さらに、「カスタム変数(値01)」は、「201303」と「前方一致」させる。

スクリーンショット 2013-03-30 1.50.34

これで、2013年3月に初訪問した人たちが、サイトにどれだけ再訪問し、どう利用しているかを把握できるようになる。

スクリーンショット 2013-03-30 1.58.36

参考にしたサイト:

アクセス分析の有意性を高めるためにKISSmetricsがコホート分析を導入 | TechCrunch Japan
http://jp.techcrunch.com/2011/09/24/20110923kissmetrics-helps-you-hone-in-on-stats-that-actually-matter-with-cohort-reports/

How to do Cohort Analysis in Google Analytics | Jonathon Balogh
http://jonathonbalogh.com/2012/04/01/how-to-do-cohort-analysis-in-google-analytics/

How to do Cohort Analysis in Google Analytics | Jonathon Balogh
http://jonathonbalogh.com/2012/04/01/how-to-do-cohort-analysis-in-google-analytics/

Google Analyticsで、新規訪問の日を記録するには? – QA@IT
http://qa.atmarkit.co.jp/q/2274

Custom Variables – Web Tracking (ga.js) – Google Analytics — Google Developers
https://developers.google.com/analytics/devguides/collection/gajs/gaTrackingCustomVariables

カスタム変数を作るコードは、Gistにもあげておいた。

try {
var f = function (d) {
return d.getFullYear() + ("0" + (d.getMonth() + 1)).slice(-2) + ("0" + d.getDate()).slice(-2);
};
var c = document.cookie;
var i = c.indexOf('__utma');
var s = null;
if (i < 0) {
s = f(new Date());
} else {
var utma = c.substring(i, c.indexOf(';', i));
if (utma) {
var v = utma.split('.');
if (v[2]) {
var date = new Date();
date.setTime(v[2] * 1000);
s = f(date);
}
}
}
if(s) {
_gaq.push(['_setCustomVar', 1, 'FirstVisit', s, 1]);
}
} catch (e) {
}
view raw ga-cohort.js hosted with ❤ by GitHub

jsdo.itでProcessingを動かす

メディアアートのためのプログラミング言語ProcessingProcessing.jsを使うことで、作った作品をJavaScriptのコミュニティサイトjsdo.itに公開することができる。

jsdo.itでの手順は次の通り。

(1) jsdo.itで新しいコードを書き始める(Start Coding)

スクリーンショット 2013-03-21 13.14.10

(2) JavaScriptのライブラリ追加(Add Library)

スクリーンショット 2013-03-21 13.16.46

(3) Processing.js v1.4.0 – js を選択して Add

(4) HTMLタブに以下のテンプレートを記述

<script type="application/processing" data-processing-target="pjs">
//Processing code is here
</script>
<canvas id="pjs"> </canvas>

(5) //Processing code is here のところに、Processingのコードを記述

このやり方では、JavaScriptタブにはコードを書かない。

Built with Processing[Ver. 1.x対応版] -デザイン/アートのためのプログラミング入門 3-6-4 力をためる、をベースに作ったコードを jsdo.it で公開してみた。

力をためる – jsdo.it
スクリーンショット 2013-03-21 13.03.26

Built with Processingはとても丁寧な解説で、プログラミング自体の入門書としてもオススメ。小学校4年生の娘もこの本(の前半)を使って自習していて、思い通りに動いたときには喜びに身悶えしてる。

JavaScript Development Tools – JavaScript開発の効率アップ

id:Layzie さんの発表「JavaScript Development Tools – JavaScript開発の効率アップ」がためになったので、プレイリストを作ってみました。(xx:xx) をクリックすると、該当する動画に飛びます。

Layzieさんによる発表への補足記事はこちらです。

expressで作ったHelloWorldをsupertestでテストする

node.jsのためのWebアプリケーションフレームワークexpress。主にURLルーティングのテストには supertest が便利。

(1) expressでHelloWorld
express の Getting started に従って、HelloWorldを表示するWebアプリを作ってみる。

$npm install express
$vi app.js

var express = require('express'),
    app = express();

app.get('/hello.txt', function (req, res) {
    res.send('Hello World');
}); 
app.listen(3000, '0.0.0.0');

$node app.js

app#send を使うと、Content-Length を自動的に追加してくれる。
ブラウザから http://localhost:3000/hello.txt にアクセスすると、Hello World と表示される。

(2) mocha と supertest でテストを書く
mocha は単体テストのためのフレームワーク。supertest と組み合わせて使うことで、express で作った Web アプリの動作を、HTTPリクエストレベルで確認できる。

はじめに、単体テストができるように app.js を修正しておく。

var express = require('express'),
    app = express();

app.get('/hello.txt', function (req, res) {
    res.send('Hello World');
}); 

module.exports = app;

if (!module.parent) {
    app.listen(3000, '0.0.0.0');
}

8行目で単体テスト側からWebアプリのロジックにアクセスできるよう、エクスポートしている。
10行目は単体テスト経由でWebアプリを動作させている場合には listen させないようにしている(単体テスト経由の場合 module.parent に値が入る)。

次に単体テスト側を準備する。
mocha はインストール済み($npm install mocha)だと手順は次の通り。

$npm install supertest
$mkdir test
$vi test/test_app.js

var request = require('supertest');
var app = require('../app');

describe('helloworld', function () {
    it('should return 200 with Content-Length', function () {
        var expected_body = 'Hello World';
        request(app)
            .get('/hello.txt')
            .expect('Content-Length', expected_body.length)
            .expect(200)
            .end(function (err, res) {
                if (err) {
                    throw err;
                }
            });
    });
});

$node_modules/mocha/bin/mocha --reporter spec

今回は実装を作ってからテストしたけれど、テスト駆動開発にも役立ちそう。

Mac OS X と homebrew なら驚くほど簡単に node.js で HelloWorld

あっという間に動かせてしまった。

(1) Mac OS X にパッケージ管理 homebrew をインストールする。
$ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"

(2) homebrew を使って node.js をインストールする。
$brew install node

(3) HelloWorldを出力するコードを書く。
コードは nodejs.org から引用。
$vi server.js

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

(4) node.js を実行する。
ブラウザでhttp://127.0.0.1:1337/を開くと Hello World と表示される。
$node server.js

便利だなぁ。
環境は以下の通り。
$sw_vers
ProductName: Mac OS X
ProductVersion: 10.7.5
BuildVersion: 11G63

補足
node.js と一緒にパッケージ管理 npm もインストールされる。
We recommend prepending the following path to your PATH environment
variable to have npm-installed binaries picked up:
/usr/local/share/npm/bin

とのことなので、
$vi ~/.bashrc
export PATH=$PATH:/usr/local/share/npm/bin
$source ~/.bashrc

としてあげる。

おまけ
node.js で使えるテストフレームワークはとても数が多い。
mocha(モカ)と should を使うなら、こんな感じ。
コードは mocha から引用。
$npm install mocha should
$mkdir test
$vi test/test.js

var should = require('should');

describe('Array', function () {
    describe('#indexOf()', function () {
        it('should return -1 when the value is not present', function () {
            [1, 2, 3].indexOf(5).should.equal(-1);
            [1, 2, 3].indexOf(0).should.equal(-1);
        });
    });
});


$node_modules/mocha/bin/mocha --reporter spec
Array
#indexOf()
✔ should return -1 when the value is not present

✔ 1 test complete (2 ms)

Google Maps API v3 と jsdo.it を使ってツイートを地図上に表示する

Google Maps API v3 と jsdo.it を使って、地図上にその位置でつぶやかれたツイートを表示するアプリを作った。

ツイートを地図上に表示(コメント付) – Google Maps JavaScript API v3 – jsdo.it – Share JavaScript, HTML5 and CSS
http://jsdo.it/takatama/zNmW

ソースコードについて解説する。

(1) ツイートの取得
GET search | Twitter Developersでツイートを取得する。JSONPを使ってブラウザ上から取得ができる。jQueryのgetJSONメソッドを使った。
ポイントはurlのquery文字列にcallback=?を加えること。?の部分はjQueryがよきにはからってくれる。

//緯度経度、半径(km)、ツイートに含めたい文字を指定して、ツイートを取得する。
function fetchTweets(latLng, query, radiusKM) {
    var url = 'http://search.twitter.com/search.json?callback=?';
    var data = {
        q: query,
        rpp: 100, 
        geocode: latLng.lat() + ',' + latLng.lng() + ',' + radiusKM
    };
    $.getJSON(url, data, function (data, status) {
        if (status === 'success') {
            addTweetsAsMarkers(data);
        } else {
            alert('twiter error');
        }
    });
}

(2) ツイートをマーカーとして地図に追加
取得したツイートをマーカーとして追加する。マーカーがクリックされたときに情報ウィンドウを開くためのコールバックを追加しておく。
コールバック関数内ではクリックされたマーカーはthisで参照できるところがポイント。

//マーカーとして未追加で、緯度経度を含むツイートのみ追加する。
function addTweetsAsMarkers(data) {
    var i;
    for (i = 0; i < data.results.length; i++) {
        var id = data.results[i].id_str;
        var geo = data.results[i].geo;
        if (!markersHash[id] && geo && geo.type === 'Point') {
            var marker = new google.maps.Marker({
                map: map,
                position: new google.maps.LatLng(parseFloat(geo.coordinates[0]), parseFloat(geo.coordinates[1])),
                title: '@' + data.results[i].from_user,
                icon: data.results[i].profile_image_url,
                result: data.results[i] //情報ウィンドウにツイートを表示するために使う
            });
            google.maps.event.addListener(marker, 'click', function() {
                //クリックされたmarkerはthisで参照できる。
                showInfoWindow(this);
            });
            markersHash[id] = marker;
            markersArray.push(marker);
        }
    }
}

(3) 情報ウィンドウのコンテンツを作成し、表示
情報ウィンドウを開く際、取得したツイートから表示する内容を作る。ツイートのテキストに http:// から始まるリンクが含まれている場合に、クリック可能にする。
JavaScript の replace メソッドにコールバック関数を渡すこともできる。arguments[1] には正規表現でグルーピングした値が入っている。詳しくはjavascriptのreplaceにfunctionを渡す – Webtech Walkerを参照のこと。

//情報ウィンドウを表示する
function showInfoWindow(marker) {
    marker.setZIndex(9999); //表示位置を一番上にする
    var linkToTheTweet = '<a target="_blank" href="http://twitter.com/' + marker.result.from_user
        + '/status/' + marker.result.id_str + '"> 開く </a></p>';
    //ツイート内のリンクをクリック可能にする。
    var text = marker.result.text.replace(/(https?:\/\/[\x21-\x7e]+)/gi, function () {
         var uri = arguments[1];
         return '<a target="_blank" href="' + uri + '">' + decodeURI(uri) + '</a>';
     });
     infoWindow.content = '<p style="font-size:0.9em;">' + text + '<div style="font-size:0.7em;">'
         + linkToTheTweet + '</div></p>';
     infoWindow.open(map, marker);
}

(4) 検索する範囲を計算し、ツイートを取得
位置情報に基づいてツイートを取得する際は、中心となる緯度・経度に加え、検索する半径も指定する必要がある。
表示している地図の北東、南西の2点の位置はMapのgetBounds()メソッドで取得できる。また、2点間の距離はGeometry Libraryのgoogle.maps.geometry.spherical.computeDistanceBetween()関数で計算できる。 ポイントはhtmlのscriptタグでGoogle Maps API を呼び出す際、srcのquery文字列に&libraries=geometryを入れること。

//検索する範囲を表示画面の大きさから計算して、ツイートを取得する。
function fetchTweetsWithRadius() {
    var distanceKM = 3.0;
    var bounds = map.getBounds();
    if(bounds) {
        var latlng = new google.maps.LatLng(bounds.getSouthWest().lat(), bounds.getNorthEast().lng());
        var distanceM = google.maps.geometry.spherical.computeDistanceBetween(bounds.getNorthEast(), latlng);
        distanceKM = Math.floor(distanceM / 2 / 100) / 10;
    }
    fetchTweets(map.getCenter(), $('#filter').val(), distanceKM + 'km');
}

(5) 地図の操作に対する動作
ここまででツイートの検索からマーカーとしての表示までは説明できた。後は各種操作のための初期化について説明する。
ここではgoogle.maps.event.addListenerを使って、地図上での操作に対する処理を定義している。
また、自動的にツイートの検索と表示を実行するため、fetchTweetsWithRadius と showNextTweet を定期的に実行する。

//ページが読み込み終わったら実行する。
function initialize() {
    var mapOptions = {
        center: new google.maps.LatLng(35.698619, 139.773288),
        zoom: 11,
        mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
    infoWindow = new google.maps.InfoWindow();
    //地図内をクリックしたら情報ウィンドウを閉じる
    google.maps.event.addListener(map, 'click', function () {
        infoWindow.close();
    });  
    //ドラッグが終わった位置で、ツイートを検索する。
    google.maps.event.addListener(map, 'dragend', function () {
         fetchTweetsWithRadius();
    });
    //ダブルクリックしズームした位置で、ツイートを検索する。
    google.maps.event.addListener(map, 'dblclick', function () {
         fetchTweetsWithRadius();
    });
    fetchTweetsWithRadius();
    setInterval(fetchTweetsWithRadius, 30000); //30秒ごとにツイートを検索する。
    setInterval(showNextTweet, 4000); //4秒ごとに情報ウィンドウを表示する。
    geocoder = new google.maps.Geocoder();
}

(6) ツイートの自動表示
あらかじめ取得していたツイートを定期的に表示していく。
マーカーが枠内に含まれている場合にのみ、表示するようにしている。

//追加したマーカーが画面上にあれば情報ウィンドウを表示する。
var currentMarker = null;
function showNextTweet() {
    if (currentMarker) {
        currentMarker.setMap(null);
        infoWindow.close();
        currentMarker = null;
    }
    var marker = markersArray.shift();
    while (marker) {
        var bounds = map.getBounds();
        //はじめに表示したときには、boundsがundefinedになってしまう。
        if (!bounds || bounds.contains(marker.getPosition())) {
            map.panTo(marker.getPosition());
            showInfoWindow(marker);
            currentMarker = marker;
            return;
        } else {
            marker.setMap(null);
            marker = markersArray.shift();
        }
    }
}

(7) 地名を入力して移動
地図の上部に設置しているフォームの操作用。入力した地名を Geocoding APIで緯度経度に変換し、地図を移動させた上で、その地点でのツイートを検索する。

//地名から緯度経度を取得して、地図を移動させ、ツイートを検索する。
function codeAddress() {
    geocoder.geocode({
        'address': $('#address').val()
    }, function(results, status) {
        if (status == google.maps.GeocoderStatus.OK) {
            map.setCenter(results[0].geometry.location);
            fetchTweetsWithRadius();
        } else {
            alert("Geocode was not successful for the following reason: " + status);
        }
    });
}

(8) 検索したツイートすべてを消去して、再検索
地図の上部に設置しているフォームの操作用。ツイートに含めたい文字を変更したり、クリアした場合に呼び出す。検索したツイートから追加してきたマーカーをすべて消去して、現在地点でのツイートを検索しなおす。

//すべてのマーカーを削除してツイートを検索する。
function reload() {
    var i;
    for (i = 0; i < markersArray.length; i++) {
        markersArray[i].setMap(null);
    }
    markersArray.length = 0;
    markersHash = {};
    fetchTweetsWithRadius();
}

(9) フォームをクリアした上で、再読み込み
地図の上部に設置しているフォームの操作用。フォームの文字列をクリアした上で、ツイートの再読み込みを行う。

//ツイートをフィルターする文字列をクリアし、ツイートを検索する。
function clearFilter() {
    $('#filter').val('');
    reload();
 }

jsdo.it でお手軽に Google Maps JavaScript API を試す

Web ブラウザ上で JavaScript, HTML, CSS のコードを書き、そのまま公開できる jsdo.it で、Google Maps JavaScript API v3Getting Started に掲載されている Hello, World を試してみた。

Hello World – Google Maps JavaScript API v3
http://jsdo.it/takatama/jRDt
指定した緯度経度の地図を表示する、ごくごく簡単なもの。

前提
Google のアカウントを持っていること。
jsdo.it のアカウントを持っていること。

手順
(1) https://code.google.com/apis/console/ にアクセスする。ログインが求められる。ログイン後、Create Project… ボタンを押す。

(2) 右ペインの Services を選択すると、利用可能な API の一覧が表示される。Google Maps API v3 を ON にする。

なお、Google Maps API は一日あたり25,000リクエストまでは無料で、1,000リクエスト超過ごとに$4かかる (2012/08/25時点) 。詳しくは地図読み込み回数の超過分のオンライン購入価格を参照。

(3) terms of service の内容をよく確認し、同意するのであれば I agree to these terms. がチェックされているのを確認の上、Accept ボタンを押す。

(4) https://code.google.com/apis/console/ に戻り、右ペインの API Access を選択する。Simple API Access に API key: が表示されているのでメモを取っておく。

なお、デフォルトの設定だとどのサイトに置いた JavaScriptからでもこの API key が利用可能になっている。今回は jsdo.it でのみ実行を許可するため Edit allowed referers…をクリックして、
jsrun.it/*
を追加しておく(jsdo.it のスクリプトは http://jsrun.it/ に配置されている)。

(5) Hello, World の例 (html) を jsdo.it にコピーして、src=”http://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&sensor=SET_TO_TRUE_OR_FALSE&#8221;
の部分を修正する。

YOUR_API_KEY を先ほど取得した API key に変更する。
SET_TO_TRUE_OR_FALSE を false に変更する。大文字の FALSE だと動作しないので注意する。

(6) 表示する地図を調整する。今回は東京駅を表示するために、
center: new google.maps.LatLng(35.681382, 139.766084),
zoom: 14,
とした。