phantomjs+qunitテストランナー

phantomjsからqunit実行したいなと思ったらphantomjsのexampleにあった。
https://github.com/ariya/phantomjs/blob/master/examples/run-qunit.js

これだとテスト数、成功数、失敗数しか出力されないのでもうすこし詳細がでるのがほしいなと思ってこのソースを参考に練習がてら自分で作ってみた。

var MAX_TIMEOUT = 3000;

function testCompleted() {
    var el = document.getElementById("qunit-testresult"),
        text = el.textContent;

    return (text && text.match("completed"));
}

function printResult() {
    var elem = document.getElementById("qunit-testresult"),
        passed = elem.querySelector(".passed").textContent;
        failed = elem.querySelector(".failed").textContent;
        total = elem.querySelector(".total").textContent;

    console.log("total: " + total + " passed: " + passed + " failed: " + failed);

    return (failed == 0);
}

function printResultDetail() {
    var slice = Array.prototype.slice,
        tests = slice.call(document.getElementById("qunit-tests").childNodes);


    tests.forEach(function (elem) {
        var moduleName = elem.querySelector(".module-name").textContent,
            testName = elem.querySelector(".test-name").textContent,
            passed = elem.querySelector(".passed").textContent,
            failed = elem.querySelector(".failed").textContent;

        console.log(moduleName + ": " + testName + "(" + passed + "," + failed + ")");

        lists = slice.call(elem.getElementsByTagName("ol").item(0).childNodes);
        lists.forEach(function (elem) {
            var msg, el;
            msg = "[" + elem.className + "]";
            if (elem.firstChild.nodeType == 3) {
                msg += elem.textContent;
            } else {
                if (el = elem.querySelector(".test-message")) {
                    msg += el.textContent;
                }
            }
            console.log("    " + msg);

            if (el = elem.querySelector(".test-expected")) {
                msg = el.getElementsByTagName("pre").item(0).textContent;
                console.log("        Expected: " + msg);
            }
            if (el = elem.querySelector(".test-actual")) {
                msg = el.getElementsByTagName("pre").item(0).textContent;
                console.log("        Result: " + msg);
            }
        });
    });
}

if (phantom.args.length === 0 || phantom.args.length > 2) {
    console.log("Usage: phantomjs-qunit-runner.js url [debug]");
    phantom.exit(0);
}

var url = phantom.args[0];
var isDebug = !!phantom.args[1];

var page = new WebPage();
page.onConsoleMessage = function (msg) {
    console.log(msg);
};

page.open(url, function (status) {
    if (status !== "success") {
        console.log("network error");
        phantom.exit(1);
    }
    var passed = false;
    var start = new Date().getTime();
    var timer = setInterval(function () {
        if (new Date().getTime() - start > MAX_TIMEOUT) {
            clearInterval(timer);
            console.log("timeout");
            phantom.exit(1);
        }
        if (page.evaluate(testCompleted)) {
            console.log("Test completed");
            passed = page.evaluate(printResult);
            if (isDebug) {
                page.evaluate(printResultDetail);
            }
            clearInterval(timer);
            if (passed) {
                phantom.exit(0);
            } else {
                phantom.exit(1);
            }
        }
        console.log("Test Running...");
    }, 100);
});

武士の家計簿をみつつダラダラ書いたのであんまり綺麗ではない。
パラメータで詳細を出すか出さないか制御できるようにしてみた。
詳細がでるようにするとこんな感じでモジュール名、テスト名、メッセージ、テスト結果とかを出力するようにした。

Test completed
total: 4 passed: 2 failed: 2
module1: test1(2,0)
    [pass]hoge
    [pass]piyo
        Expected: 1
module1: test2(0,2)
    [fail]hoge
    [fail]piyo
        Expected: [
  1,
  2,
  3
]
        Result: [
  1,
  2,
  4
]
module2: test3(0,0)

全部のassertの出力で試してないのでもしかしたら正しく値を拾えない場合があるかも。

qCanverのソース上げた

この前書いたライブラリ
http://d.hatena.ne.jp/daisun/20110723/1311405757

ソースをgithubに上げた。
https://github.com/daisun/qcanver

テストも未完成なのでたぶん普通にエラーになる箇所があるし
コードも使われてないようなのが混ざっている気がする。

今後はなるべくgithubにソース上げるようにしようとか思っている。

手前味噌。Canvasライブラリ「qCanver」作成中。

もろきゅう。

半年以上前からこつこつと勉強がてら作っていCanvasを扱うためのライブラリ。
ライブラリの名前は、jQueryっぽいくCanvasを文字っててちょっとトンチがきいてそうな名前を考えていて、息子がきゅうり好きなのもあって「qCanver」(これだとキューキャンバーな気がするけど気にしない。)とした。

テストと一部クリアしなきゃいけない不具合とかあるので今は公開はしない。
そのうちする予定。

勉強がてらといったけど、下記本を見ながらCanvasの使い方を勉強しつつ作った。

徹底解説HTML5APIガイドブック ビジュアル系API編

徹底解説HTML5APIガイドブック ビジュアル系API編

といってもすげー便利なライブラリとかでは全然なくむしろライブラリとか呼ぶのすらおこがましいほど。ただのラッパーです。
なので、基本Canvasを使う上でのメソッドをラップしてメソッドチェーンでかけるようにしただけ。

//idがcanvasのcanvasがある想定。
//普通に書く
var ctx = document.getElementById("canvas").getContext("2d");
ctx.beginPath();
ctx.moveTo(50, 10);
ctx.lineTo(10, 75);
ctx.lineTo(90, 75);
ctx.fill();

//qCanver使って同じことする
qCanver("canvas")
    .begin(50, 10)
    .line(10, 75)
    .line(90, 75)
    .draw("fill");

というように本当にラップしただけだ。

円書くくらいは1メソッドできるようにまとめた。

//普通にかく
var ctx = document.getElementById("canvas").getContext("2d");
ctx.arc(30, 30, 20, 0, Math.PI * 2, true);
ctx.fill();

//qCanver使って同じことする
qCanver("canvas").fillCircle(30, 30, 20, true);

関数化するとcontextを引数とかで渡さないといけなかったりするけど、

var fillTriangle = function (ctx) {
    this.begin(50, 10)
        .line(10, 75)
        .line(90, 75)
        .draw("fill");
    ctx.fillRect(10, 10, 20, 20);
};
qCanver("canvas").execute(fillTriangle);//qCanver("canvas").draw(fillTriangle)でも同じ

とthisにqCanverオブジェクトに設定して関数を実行できるメソッドを用意した。
さらに第一引数にはcontextが勝手にわたるのでqCanverを使わずに、contextを使って普通にcanvasを書くことも可能。

drawメソッドに引数を渡さないと、オレオレdrawイベントを発火させるようにできる。
オレオレイベントはobserveメソッドで設定。
ボタンクリックしたら表示するようなの書くと。

//fillTriangleはさっきの。
qCanver("canvas").observe("draw", fillTriangle);
document.getElementById("btn").onclick = function () {
    qCanver("canvas").draw();
};

一応、アニメーションも用意した。

// 四角が横に移動して適当なとこで止まるようなの書くと
qCanver("canvas").animate({
    x: 0,
    y: 0,
    offsetX: 5,
    offsetY: 0,
    duration: 50,
    clear: true
}, function (ctx, x, y) {
    this.fillRect(x, y, 10, 10);
    if (x > 100) {
        this.stopAnimate();
    }
});

色を指定するのにrgb(255, 0, 0)みたいに設定するのに

var r = 255;
var g = 0;
var b = 0;
var rgb = "rgb(" + r + "," + g + "," + b + ")";

var r = 255;
var g = 0;
var b = 0;
var rgb = qCanver.toRGBString(r, g, b);

とかする地味なメソッドも用意してる。

そしてこれがこのライブラリ1番のうり?
レイヤー機能を作れる。
内部的には新しいcanvas要素生成して親のcanvas要素にdrawImageで表示してるだけなんだけど。
このやり方は本に載ってたの見て知った。

qCanver("canvas")
    .setLayer("rect1", function () {
        this.fillRect(50, 50, 20, 20);
    })
    .setLayer("circle1", function () {
        this.fillCircle(100, 100, 20, true);
    })
    .drawLayer();

四角と円が表示されるけど、実際は別なcanvas要素に表示している。
アニメーションもできる。
別なcanvasに書いてあると、アニメーションの速度が
それぞれ設定できたりしてよい。

qCanver("canvas")
    .setLayer("rect1", function () {
        this.animate({
            offsetX: 5,
            duration: 50
        }, function (ctx, x, y) {
            this.fillRect(x, y, 20, 20);
        });
    })
    .setLayer("circle1", function () {
        this.animate({
            x: 30,
            offsetY: 5,
            duration: 100
        }, function (ctx, x, y) {
            this.fillCircle(x, y, 20, true);
        });
    })
    .animateLayer();

これ実行すると四角が横にさらっと、円が下にゆっくりと移動していく。

とまあ、こんなのを作っている。

ちなみに開発環境は、
モジュール単位にjsを書いて前はcatするだけのshellを書いて組み立ててたけど、
今は、Makefileつくって1つのjsにまとめてる。
テストはqunitを使っているけど、canvasに円がちゃんと書かれるかのテストとかどうしたらいいか迷ってる。
リポジトリはgitを使っている。

これ書いてたら早く完成させたくなってきたのでがんばることにした。

Canvasの幅と高さ

Canvasはwidthとheight属性を指定しないと、widthが300pxでheightが150pxになる。
じゃあ、CSSでwidthとheightしたらどうなるんだろうか?と思って指定してみると、描画できるcanvas自体はwidth属性とheight属性でしたいした領域になる。
それでCSSで指定した幅でcanvasが広がって(伸ばされて?)表示されるみたい。

これ表示してみると

<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <style>
            canvas {
                width: 400px;
                height: 400px;
            }
        </style>
        <title>てすと</title>
    </head>
    <body>
        <canvas id="canvas"></canvas>
        <script>
            (function () {
                var ctx = document.getElementById("canvas").getContext("2d");
                ctx.rect(50, 50, 20, 20);
                ctx.fill();
            })();
        </script>
    </body>
</html>

正方形書いてるけど、CSSでwidthとheightを400pxって指定すると幅と高さの比率が変わるので間延びして長方形を書いたように見える。

Canvasの幅と高さ取るのに、widthでいいのかclientWidthなのかgetBoundingClientRectでとったほうがいいのか。paddingとかmargin設定したらだめじゃんとかいろいろ迷ったけど、どうやら素直にwidthとheightを使えばいいっぽいな。

あの問題のやつをJSでかくと

久しぶりに書いとく。

昨日でてきたこれを

((lambda (n)
  ((lambda (fact)
    (fact fact n))
  (lambda (ft k)
    (if (= k 1)
        1
        (* k (ft ft (- k 1)))))))
10)

JavaScriptにすると。

(function (n) {
    return (function (fact) {
        return fact(fact, n);
    })(function (ft, k) {
        if (k === 1) {
            return 1;
        } else {
            return k * ft(ft, (k - 1));
        }
    });
})(10);

こんなか?

defer属性の挙動

久しぶりに更新。

defer属性の挙動について、ふと気になったので調べる。

defer属性はのっぺりひらたく言うと、スクリプトの実行を延期させるようにすること。
最近はパフォーマンスを改善するのにも使われるようです。
サイ本いわく対応しているのはIEのみとのことなので他がどうなっているか
を調べてみる。

下記ソースを実行すると

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <meta http-equiv="content-style-type" content="text/css">
    <meta http-equiv="content-script-type" content="text/javascript">
    <title>テスト</title>
    <script type="text/javascript" defer="defer">
        alert("1");
    </script>
</head>
<body>
<script type="text/javascript">
    alert("2");
</script>
</body>
</html>

想定する動きとしては
2->1
の順でアラートが表示されるはず。

調べる環境は手元にある

  • IE8
  • Firefox3.6
  • Safari5
  • chrome7

で、実行結果。

ブラウザ 結果
IE8
Firefox3.6 ×
Safari5 ×
chrome7 ×

ここからが不思議なところ(?)
外部ファイルに設定したものにdefer属性をつけてもためしてみたら

ブラウザ 結果
IE8
Firefox3.6
Safari5 ×
chrome7

Firefoxchromeが2->1の順でアラートがでるようになった。
調べ方が悪いのかなんなのかちょっとわからないけどこうなった。

Workerをちょっとだけさわった。

ちょっとWorkerを使ってみようかと思って試してみた。

使い方

var worker = new Worker("worker.js");

みたいにする。
workerにお仕事してもらう内容は、worker.jsって別ファイルに記述する。

worker.js

onmessage = function (evt) {
    var data = evt.data
    //お仕事
}

onmessageイベントをトリガーに処理をさせるらしい。
で、workerと呼び出し元の親とのやり取りは

worker.postMessage(/* 渡したいデータ */);

みたくする。
受け取りはeventオブジェクトのdataプロパティから取り出せる。
どうやらここで渡せるのはchromeだと文字列になってしまうらしい。
なので、objectをわたすと[object object]になってしまう。
FFだとうまくいった。
あとそもそもFunctionは渡せない。
データの受け渡しならJSON.stringifyとJSON.parseをかませて
受け渡すようにすればどうにか解消される。

応用編?

用途や処理内容によると思うけど、
1worker(ファイル)に複数のお仕事ができるようにさせてみる
(そもそも、workerは重い処理をさせる想定でこういったつくりになっているから
今からやるやり方は正しいやり方(使い方?)とはいえないと思う)

workerで足し算してみる(本来はこんな軽い処理で使わないけど)
var worker = new Worker("worker.js");
worker.postMessage([2, 3]);
worker.onmessage = function (evt) {
    alert(evt.data);
};

worker.js

onmessage = function (evt) {
    postMessage(evt.data[0] + evt.data[1]);
};
これに引き算もできるようにしてみる
var worker = new Worker("worker.js");
worker.postMessage(["add", 2, 3]);// 足し算
worker.postMessage(["sub", 2, 3]);// 引き算
worker.onmessage = function (evt) {
    alert(evt.data);
};

worker.js

var works = {
    "add": function (a, b) {
        return a + b;
    },
    "sub": function (a, b) {
        return a - b;
    }
};
onmessage = function (evt) {
    var data = evt.data;
    var work = data.shift();
    var args = data;
    if (works[work]) {
       postMessage(works[work].apply(this, args));
    } else {
       postMessage(null);
    }
};

こうすれば割とできる。
これだと何の結果が戻ってきているのかわからないので
worker.js同様なんらかの目印をつけて処理を分岐する必要はある。

とりあえずこんな感じだ。

workerは呼び出し側とworker側で似たようなことを書かないと
いけないのがちょっと不便と感じた。