5分でわかるWatir

以下の文章はŽeljko Filipinによる"Watir in five minutes"の翻訳です。*1




(中略)


僕が初めてWatirを仕事で使った時、すごく驚いたことを今でも覚えている。インストールしてから数時間で、僕はテスト中のWebアプリケーション用のスクリプトを書けるようになっていた。この本を読めば、数時間もかからずにWebサイトをテストできるようになるはずだ。


もしRubyに親しんでいるのなら、IRBRubyライブラリを勉強するための最良のツールの1つだってことがわかっているだろう。
もしRubyを知らないのなら、こう思うかもしれない。IRBって何?
IRB(この場合)はInternational Ruby BoardでもImmigration or Refugee Board (of Canada)のことでもない。Interactive Ruby Sehllのことだ。読んで字の如く、Ruby用のシェルことだと思ってくれ。


IRBをスタートするためには、コマンドライン上で'irb'と打てばいい。このように表示されるはずだ。

$ irb
>

これで、Rubyコマンドを打ち込めば、即座に結果が得られるようになった。'require "watir-webdriver"と入力してwatir-webdriver gemを使用することをRubyに伝えてみよう。
このように表示されるはずだ。

> require "watir-webdriver"
=> true

こうなっても、パニクるな。

> require "watir-webdriver"
LoadError: no such file to load -- watir-webdriver
from (irb):1:in `require'
from (irb):1


これは最初にRubyGemsをrequireしなければならないことを意味している。この場合は、このようにする。

> require "rubygems"
=> true

> require "watir-webdriver"
=> true

すべてのRubyコマンドは何らかの値を返す。`require "rubygems"`は、Rubyのインストール状況に応じて'true'または'false'を返す。今のところは気にしなくていい。`require "watir-webdriver"`の返り値は`=> true`となるはずだ。返り値には2つの部分がある。最初のは'=>'。矢印みたいなこれは、'Rubyがこれを返した'ということを意味している。2つめの部分は'true'。'true'が返ってきた場合、すべてが上手く行っている。
こうは言ったけれども、とりあえず返り値のことは無視してくれてかまわない。


さてこれからが魔法の始まりだ。この1つのコマンドでFirefoxが立ち上がる。

browser = Watir::Browser.new :ff

https://github.com/ninoseki/watirbook/raw/master/images/watir-in-five-minutes/webdriver-firefox.png
watir-webdriver driving Firefox 6 on Mac OS 10.6


(中略)


立ち上げるブラウザは1つだけにしておこう。それで十分だ。他のブラウザとは後で遊ぼう。出力はこうなるはずだ。

> browser = Watir::Browser.new :ff
=> #<Watir::Browser:0x2ed1f1cd5b186306 url="about:blank" title="">

前にも言ったように、`#`のところは無視してかまわない。立ち上げたFirefoxがBrowserオブジェクトとして返り値となったものの、テキスト表現だ。


ただブラウザを立ち上げただけでもカッコいいけど、まだ便利ではない。Watirはそれ以上のことができる。例えば、ブラウザをどんなサイトへも誘導することができる。今回の例として、google.comを使用する。この例を終えてから、他のサイトへも自分自身で試してもらいたい。


さて、google.comへ行こう。

> browser.goto "http://www.google.com/"
=> "http://www.google.hr/"

google.comが開かれたね。魔法みたいじゃないかい?


ブラウザをコントロールすることはとても便利だ。だけどもちろん、テストをするためにはアクションを実行するだけでは足りない。アクションの後に何が起きたのか確認しなければいけないんだ。ブラウザのアドレスバーにURLを入力した後に何が起きるか、リンクボタンをクリックし後に何が起きるか、テキストフィールドに何か入力したりセレクトボックスで何かを選択した場合に何が起こるのか・・・?


これから初めて確認を行う。同じように、初めてコマンドの後にRubyが何を返したのかについても調べて見る。ブラウザがgoogle.comを実際に開いたか確認してみよう。

> browser.url
=> "http://www.google.hr/"

ちゃんと動いてたね!Rubyはブラウザのアドレスバーの文字列を返した。僕はクロアチアに住んでいるから、*google.hr*が開いた。もし君がアメリカ以外の国に住んでいるなら、別のGoogleサイトを開いているだろう。


リンクをクリックするときがきた。どのリンクをクリックするのか、簡単に明示することができる。今は、Google.com in Englishというテキストのリンクをクリックしてみる。同じテキストのリンクが同一ページ内に複数あると少し複雑になるけど、これに関しては後で扱う。もし既にgoogle.comを開いているなら、このステップは無視してくれ。

> browser.link(:text => "Google.com in English").click
=> []

さて、google.comが開いた。

別のリンクをクリックする前に、Watirのすごい特徴をお見せしよう。それはフラッシュという。現実のWebアプリケーションは複雑だ。時には既存のもののテストやデバッグを行わければいけない。正しい要素をやり取りしているのか、確認しないといけない。試してみよう(google.comの左上にあるImagesリンクを見て)。

> browser.link(:text => "Images").flash
=> 10

https://github.com/ninoseki/watirbook/raw/master/images/watir-in-five-minutes/flash-1.png
https://github.com/ninoseki/watirbook/raw/master/images/watir-in-five-minutes/flash-2.png


リンクがフラッシュしたのが確認できた?バックグラウンドカラーが赤に数回変わった。クールじゃない?僕はWatirを使うときは何時もこの機能を利用する(例えばカンファレンスで)。Watirはプレゼン向きだと思う。とても視覚的だ。
もしフラッシュが確認できなかったら(フラッシュするのは短い間だ)、同じコマンドを数回実行してもらいたい。同じコマンドを実行するために上向きの矢印キーをクリックすればいい。


リンクをクリックしてみよう。

> browser.link(:text => "Images").click
=> []

今回はページのタイトルを確認してみよう。

> browser.title
=> "Google Images"

ページのタイトルが文字列で返ってきた。

なにか検索してみよう。これは検索のテキストフィールドにbookの入力を行う。

> browser.text_field(:name => "q").set "book"
=> ["book"] 

どうやって僕がテキストフィールドのname属性がqだとわかったのか不思議かもしれない(':name => "q"'のこと)。もし君がページ内を調査する方法を知らなくても、このまま読み進めてくれ。後で説明する。


さて、Search Imageボタンをクリックしよう。

> browser.button(:value => "Search Images").click
=> []

検索結果ページが表示された。ページ内に何枚画象があるのか確認してみよう。
(違う結果になる可能性もあって、常に250というわけではない)

> browser.images.size
=> 250
||< 
最後に、ブラウザを閉じてみよう。
>||
    > browser.close
=> true

さて、楽しめたね。だけど常にIRBに入力したくはないだろう。テストを実行している間に、何か他のことをやりたいでしょ。RubyWatirの他のほとんどすべてと同じく、単純な解決策がある。IRBに入力したコードをすべてテキストファイルに貼りつけて、拡張子を*rb*にして保存するんだ。IRBは開発やデバックの時にしか使わない。だから、'irb'をファイルの最初の行に貼り付けなくていい。ファイルはこのようになるはずだ。

require "watir-webdriver"
browser = Watir::Browser.new :ff
browser.goto "http://www.google.com/"
browser.url
browser.link(:text => "Google.com in English").click
browser.link(:text => "Images").click
browser.title
browser.text_field(:name => "q").set "book"
browser.button(:value => "Search Images").click
browser.images.size
browser.close

IRBに`require "rubygems"`と入力していた場合は、 ファイルの先頭にこれを付け加えておこう。

どんなテキストエディターを使用してもかまわない。僕は [RubyMine](http://www.jetbrains.com/ruby/)か[NetBeans](http://netbeans.org/)を使っている。

スクリプトを実行する。出力はこのようになるはずだ。

$ ruby watir5.rb
http://www.google.hr/
Google Images
246

後でクールな見た目のレポートの作り方を教えよう。

これまでのところで良い印象を受けなかったら、多分もう無理だ。もし気に入ってくれたのなら、どでかい大砲を持ってくる時がきた。深いところへ行こう。

PILでHalftonみたいなの

こんな感じですか?わかりません><。

Before

After

# -*- coding: utf-8 -*-
from PIL import Image, ImageDraw

def collect(img, h, w, y, x, interval):
  y2 = (y + interval) if (y + interval) < h else h - 1
  x2 = (x + interval) if (x + interval) < w else w - 1
  r = 0
  g = 0
  b = 0
  c = 0
  
  for i in range(y, y2 + 1):
    for j in range(x , x2 + 1):
      p = img.getpixel((j, i))
      r += p[0]
      g += p[1]
      b += p[2]
      c += 1
  
  return (r / c , g / c , b / c) if c > 0 else (255, 255, 255)

def halftone(img):
  size = img.size
  w = size[0]
  h = size[1]
  interval = 4
  r = interval / 2
  
  output = Image.new('RGB', (w, h), 'white')
  draw = ImageDraw.Draw(output)
  
  for y in range(0, h + r, interval):
    for x in range(0, w + r, interval):
      p = collect(img, h, w, y, x, interval)
      draw.ellipse((x - r, y - r, x + r, y + r), fill = p)
  
  return output

if __name__ == '__main__':
  img = Image.open("src.jpg")

  output = halftone(img)
  output.save("dst.jpg")

Javascriptで画象をグリッチ


Canvasの勉強がてらにJavascriptで画象をグリッチするアプリをつくってみた。
http://glitched-canvas.heroku.com/
ソースコード: https://github.com/ninoseki/glitched-canvas


Canvas要素にはピクセル単位でアクセスできるので、何でもできそうですね。



Javascript・CSSを圧縮・結合するCodeIgniterライブラリ「Simple assets」

CodeIgniterでJavascriptCSSを圧縮・結合するライブラリ「Simple assets」が便利だったので紹介してみる。
https://github.com/bstrahija/assets

導入

GitHubからダウンロードし、config、helpers、librariesをapplication配下に配置する。
※すでにapplication/config/autocload.phpが初期状態でない場合は上書きせず、追記すること。

設定

デフォルトの設定では以下のようなディレクトリ配置になっている。

/application
/assets
    /cache
    /css
    /images
    /js
/sparks
/system


この設定を変えたい場合は、application/config/assets.phpを変更すればいい。

<?php
$config['assets']['assets_dir'] = 'assets'; 
$config['assets']['js_dir'] 	= 'js';
$config['assets']['css_dir'] 	= 'css';
$config['assets']['cache_dir'] 	= 'cache';

使用方法

<?php display_css(array('init.css', 'style.css')); ?> // 引数にとったCSSを圧縮・結合し、linkタグを出力
<?php display_js(array('libs/modernizr-1.6.js', 'libs/jquery-1.4.4.js', 'plugins.js', 'script.js')); ?> // 引数にとったJavascriptを圧縮結合し、scriptタグを出力

※圧縮・結合されたファイルはassets/cache_dirに格納される。


うーん簡単で便利ですね。

コントローラー経由でファイルを出力する

すごく単純なことだけど、これまでやったことがなかったのでメモ。
ファイル名はContent-Dispositionヘッダーを使って指定するんすね。今まで知らんかった。

<?php

class Test extends CI_Controller {    
        public function files($name) {
            $this->load->helper('file');
            $this->load->library('upload');
            
            // ファイルを読み込んで出力
            $path = $this->upload->upload_path . $name;
            $data = readfile($path);
            $mime = get_mime_by_extension($path);
            
            $this->output
                ->set_content_type($mime)
                ->set_header("Content-Disposition: attachment; filename=\"{$name}\"")
                ->set_output($data);          
        }
}
?>

アップロード不可能な拡張子を指定してファイルアップロードする

CodeIgniterのファイルアップロードクラスはアップロード可能な拡張子を指定することができるけど、逆にアップロード不可能な拡張子を指定できない。
そこでCI_Uploadクラスを拡張して、アップロード不可能な拡張子を指定できるようにしてみる。


MY_Upload.php

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class MY_Upload extends CI_Upload {
    
        public $disallowed_types                = "";

	// --------------------------------------------------------------------

	/**
	 * Initialize preferences
	 *
	 * @param	array
	 * @return	void
	 */
	public function initialize($config = array())
	{
		$defaults = array(
							'max_size'			=> 0,
							'max_width'			=> 0,
							'max_height'		=> 0,
							'max_filename'		=> 0,
							'allowed_types'		=> "",
                                                        'disallowed_types'      => "",
							'file_temp'			=> "",
							'file_name'			=> "",
							'orig_name'			=> "",
							'file_type'			=> "",
							'file_size'			=> "",
							'file_ext'			=> "",
							'upload_path'		=> "",
							'overwrite'			=> FALSE,
							'encrypt_name'		=> FALSE,
							'is_image'			=> FALSE,
							'image_width'		=> '',
							'image_height'		=> '',
							'image_type'		=> '',
							'image_size_str'	=> '',
							'error_msg'			=> array(),
							'mimes'				=> array(),
							'remove_spaces'		=> TRUE,
							'xss_clean'			=> FALSE,
							'temp_prefix'		=> "temp_file_",
							'client_name'		=> ''
						);


		foreach ($defaults as $key => $val)
		{
			if (isset($config[$key]))
			{
				$method = 'set_'.$key;
				if (method_exists($this, $method))
				{
					$this->$method($config[$key]);
				}
				else
				{
					$this->$key = $config[$key];
				}
			}
			else
			{
				$this->$key = $val;
			}
		}

		// if a file_name was provided in the config, use it instead of the user input
		// supplied file name for all uploads until initialized again
		$this->_file_name_override = $this->file_name;
	}

	// --------------------------------------------------------------------


	/**
	 * Verify that the filetype is allowed
	 *
	 * @return	bool
	 */
	public function is_allowed_filetype($ignore_mime = FALSE)
	{
            // if allowed file type list is not defined
            if (count($this->allowed_types) == 0 OR ! is_array($this->allowed_types)) {
                // if disallowed file type list is not defined
                if (count($this->disallowed_types) == 0 OR ! is_array($this->disallowed_types))
                {
                    return TRUE;
                }
                // check for disallowed file types and return
                // negated because is_disallowed_filetype returns opposite result as this function
                return ! $this->is_disallowed_filetype();
            }

            // proceed as usual with allowed file type list check
            return parent::is_allowed_filetype($ignore_mime);

	}

	// --------------------------------------------------------------------

        
	/**
	 * Set Allowed File Types
	 *
	 * @param	string
	 * @return	void
	 */
	public function set_disallowed_types($types)
	{
		if ( ! is_array($types) && $types == '*')
		{
			$this->disallowed_types = '*';
			return;
		}
		$this->disallowed_types = explode('|', $types);
	}
        
        // --------------------------------------------------------------------             

	/**
	 * Verify that the filetype is disallowed
	 *
	 * @return	bool
	 */
	public function is_disallowed_filetype($ignore_mime = FALSE)
	{
		if ($this->disallowed_types == '*')
		{
			return TRUE;
		}

		if (count($this->disallowed_types) == 0 OR ! is_array($this->disallowed_types))
		{
			return FALSE;
		}

		$ext = strtolower(ltrim($this->file_ext, '.'));

		if ( in_array($ext, $this->disallowed_types))
		{
			return TRUE;
		}
                
		return FALSE;
	}         
}

以上のファイルとapplication/librariesに配置すればOK。


設定ファイルは以下のようにアップロード不可能な拡張子のみ記述するようにしておく。

<?php

$config['upload_path'] = APPPATH . '../uploads/';
$config['disallowed_types'] = 'gif|jpg|png';
$config['max_size']	= '100';
$config['max_width'] = '1024';
$config['max_height'] = '768';


こんな感じっすかね?




参考: CODEIGNITER FILE UPLOAD: SETTING DISALLOWED FILE TYPES

Backbone.jsを使ってアプリを作ったよ


http://hitorigoto.heroku.com/
Wrenライクにタイムラインを見ずにツイートだけができるアプリを、Backbone.jsを使って作ってみた。
ソースはGithubで公開してある。


Backbone.jsのチュートリアルHello Backbone.js(英語)がとてもわかりやすく参考になった。
今回程度のものだと普通にJavascriptで直書きしてもいいんだけど、Backbone.jsを使うと一貫性/保守性が向上するってことを実感。
Backbone.js面白い!