Entering Rails 4

Installing Rails using RVM:

1
2
3
4
5
rvm get stable && rvm install ruby-2.0.0

rvm --default use 2.0.0

gem install rails --version 4.0.0.rc1
Comments

Github and Multiple Oauth Provider

Allowing users to login with multiple authentication providers brings great benefits but also results in some annoying edge cases. For example, what happens when they login with one provider, logout and then login with another? What happens when they try to login with one having already logged in with another?

Typically authentication systems have a User model which handles most of the authentication logic but having multiple logins forces you to correctly separate the concepts of an Identity and a User. An Identity is a particular authentication method which a user has used to identify themselves with your site whilst a User manages data which is directly related to your site itself.

So to start you will want to create both User and Identity models. We will also add some convenience methods for creating identities and users when the OmniAuth callback is invoked:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# app/models/user.rb
class User < ActiveRecord::Base
  has_many :identities

  def self.create_with_omniauth(info)
    create(name: info['name'])
  end
end

# app/models/identity.rb
class Identity < ActiveRecord::Base
  belongs_to :user

  def self.find_with_omniauth(auth)
    find_by_provider_and_uid(auth['provider'], auth['uid'])
  end

  def self.create_with_omniauth(auth)
    create(uid: auth['uid'], provider: auth['provider'])
  end
end

So a user can have multiple identities and each identity belongs to a single user.

Next we need to handle logging in and logging out. This is managing session data since a logged in user is simply a person who has some session data confirming that they have been logged in. The OmniAuth callback which a provider will redirect to upon authenticating a user is /auth/:provider/callback so lets setup a route and a controller to handle this. We should also setup some helper methods on our Application Controller for handling the current user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# config/routes.rb
YourAppName::Application.routes.draw do
  match '/auth/:provider/callback', to: 'sessions#create'
  match '/logout', to: 'sessions#destroy'
end

#app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    # Login the User here
  end

  def destroy
    # Logout the User here
  end
end

#app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery

  protected

  def current_user
    @current_user ||= User.find_by_id(session[:user_id])
  end

  def signed_in?
    !!current_user
  end
  helper_method :current_user, :signed_in?

  def current_user=(user)
    @current_user = user
    session[:user_id] = user.nil? ? user : user.id
  end
end

Now to login, all a user needs to do is go to /auth/provider and they will get redirected to Sessions Controller create method after authenticating. So there are a number of possibilities when they hit this action:

A user has never used your site before. They have no User model and no Identities either. A user is logged out but they have logged into your site with a provider previously. They are now signing in with the same one again. Just as above but they are now signing in with a different provider. A user is logged in with a provider but they try to login with the same provider again. A user is logged in but they try to login with a different provider. The first two cases are just like a normal sign in process. The final 3 cases occur because we are allowing multiple providers and they can be tricky to handle.

Firstly, we need to grab authentication data given to us by the provider which is stored in request.env[omniauth.auth]. Then we need to check whether we have an identity which matches this data or create a new one.

How we proceed for here depends on whether the user is already logged in. If they aren’t logged in then either they are a brand new user (so we treat their request like a registration) or they already have an account (so we treat this like a login request).

If they are logged in then we treat their request like they are trying to link an identity with their account. Either they are trying to link an identity which they have already linked (in which case we should display an error message telling them that) or it is a brand new identity so we go ahead and link it.

So at this point our skeleton create method looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def create
  auth = request.env['omniauth.auth']
  # Find an identity here
  @identity = Identity.find_with_omniauth(auth)

  if @identity.nil?
    # If no identity was found, create a brand new one here
    @identity = Identity.create_with_omniauth(auth)
  end

  if signed_in?
    if @identity.user == current_user
      # User is signed in so they are trying to link an identity with their
      # account. But we found the identity and the user associated with it 
      # is the current user. So the identity is already associated with 
      # this user. So let's display an error message.
      redirect_to root_url, notice: "Already linked that account!"
    else
      # The identity is not associated with the current_user so lets 
      # associate the identity
      @identity.user = current_user
      @identity.save()
      redirect_to root_url, notice: "Successfully linked that account!"
    end
  else
    if @identity.user.present?
      # The identity we found had a user associated with it so let's 
      # just log them in here
      self.current_user = @identity.user
      redirect_to root_url, notice: "Signed in!"
    else
      # No user associated with the identity so we need to create a new one
      redirect_to new_user_url, notice: "Please finish registering"
    end
  end
end

So at this point, there are a couple of further considerations. Firstly on the signed in/identity not associated with user branch, there are two reasons why an identity might not be associated with a user. It could be that the identity is brand new, having never been used to sign in before. However, it could be that it has been used and so is already associated with a different user, although not necessarily a different person. Given that this user knew the login credentials for that identity, I think it is probably sufficiently prudent to assume that they are, in fact, the same person who also created the previous user. However, by simply reassigning the user to which the identity is associated with to the current one, you not only leave a user model potentially dangling with no identities to sign in with but also prevent the user from merging their data from their previous account in with this one. Resolving this will be dependent entirely on how much data, and the nature of that data, you have stored for each user but for sufficiently simple applications, you could at this point check to see if the old user has any identities left and, if not, delete that user. If the person using your site is likely to lose any data from this process then you would either need to make this sufficiently clear to them before proceeding or provide them with a way to migrate that data over (or handle it automatically, if possible).

Secondly, on the not signed in/no user model branch, you may need more registration data from your user than can be provided by your authentication providers. At this point, as I have assumed above, you can redirect them to a new user form and redirect them to this point if they try to access any other part of the app without completing it. Then create the user and log them in again when they have. Otherwise, if no further data is necessary or mandatory, you can go ahead and create a blank user model in the create method and log them straight in.

Finally, a few lose ends. Here is the destroy method for logging users out:

1
2
3
4
def destroy
  self.current_user = nil
  redirect_to root_url, notice: "Signed out!"
end

You’ll also find that the OmniAuth callback url does not correctly verify the rails authenticity token and so will destroy any session data upon returning, thereby logging your current user out. This will prevent them from associating a new identity with their current account. You can get around this by adding skip_before_filter :verify_authenticity_token, only: :create to your sessions controller but I am unsure of the security implications of this.

You’ll also need some migrations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users
  end
end

class CreateIdentities < ActiveRecord::Migration
  def change
    create_table :identities do |t|
      t.string :uid
      t.string :provider
      t.references :user
    end

    add_index :identities, :user_id
  end
end
Comments

Use View Helper at Controller

guide

api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
helper :foo             # => requires 'foo_helper' and includes FooHelper
helper 'resources/foo'  # => requires 'resources/foo_helper' and includes Resources::FooHelper

# One line
helper { def hello() "Hello, world!" end }

# Multi-line
helper do
  def foo(bar)
    "#{bar} is the very best"
  end
end

class ApplicationController < ActionController::Base
  helper_method :current_user, :logged_in?

  def current_user
    @current_user ||= User.find_by_id(session[:user])
  end

   def logged_in?
     current_user != nil
   end
end

The answer depends on the Rails version. Rails >= 3.1

Change the include_all_helpers config to false in any environment where you want to apply the configuration. If you want the config to apply to all environments, change it in application.rb.

1
config.action_controller.include_all_helpers = false

When false, it will skip the inclusion. Rails < 3.1

Delete the following line from ApplicationController

1
helper :all

In this way each controller will load its own helpers.

Comments

Rails Integrate With Google Analytics Api

Here is serveral usefull source:

gattica

rugalytics

ruby toolbox

Google api Doc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# Include the gem
require 'gattica'

# Login
ga = Gattica.new({
    :email => 'email@gmail.com',
    :password => 'password'
})

# Get a list of accounts
accounts = ga.accounts

# Choose the first account
ga.profile_id = accounts.first.profile_id

# Get the data
data = ga.get({
    :start_date   => '2011-01-01',
    :end_date     => '2011-04-01',
    :dimensions   => ['month', 'year'],
    :metrics      => ['visits', 'bounces'],
})

# Show the data
puts data.inspect


# Sorting by number of visits in descending order (most visits at the top)
data = ga.get({
    :start_date   => '2011-01-01',
    :end_date     => '2011-04-01',
    :dimensions   => ['month', 'year'],
    :metrics      => ['visits'],
    :sort         => ['-visits']
})


# Return visits and bounces for mobile traffic 
# (Google's default user segment gaid::-11)

mobile_traffic = ga.get({
  :start_date   => '2011-01-01',
  :end_date     => '2011-02-01',
  :dimensions   => ['month', 'year'],
  :metrics      => ['visits', 'bounces'],
  :segment      => 'gaid::-11'
})


# Filter by Firefox users
firefox_users = ga.get({
  :start_date   => '2010-01-01',
  :end_date     => '2011-01-01',
  :dimensions   => ['month', 'year'],
  :metrics      => ['visits', 'bounces'],
  :filters      => ['browser == Firefox']
})

# Filter where visits is >= 10000
lots_of_visits = ga.get({
  :start_date   => '2010-01-01',
  :end_date     => '2011-02-01',
  :dimensions   => ['month', 'year'],
  :metrics      => ['visits', 'bounces'],
  :filters      => ['visits >= 10000']
})


# Get the top 25 keywords that drove traffic
data = ga.get({
  :start_date => '2011-01-01',
  :end_date => '2011-04-01',
  :dimensions => ['keyword'],
  :metrics => ['visits'],
  :sort => ['-visits'],
  :max_results => 25
})

# Output our results
data.points.each do |data_point|
  kw = data_point.dimensions.detect { |dim| dim.key == :keyword }.value
  visits = data_point.metrics.detect { |metric| metric.key == :visits }.value
  puts "#{visits} visits => '#{kw}'"
end

# =>
#   19667 visits => '(not set)'
#   1677 visits => 'keyword 1'
#   178 visits => 'keyword 2'
#   165 visits => 'keyword 3'
#   161 visits => 'keyword 4'
#   112 visits => 'keyword 5'
#   105 visits => 'seo company reviews'
#   ...
Comments

Using Nano as a Editor

Installing(if not available) nano for mac/ubuntu

For Mac:

Download Page

1
2
3
./configure
make
sudo make install

For Ubuntu:

1
aptitude install nano

Basic commands

To create or edit a file, use the following command:

1
nano tmp.html

At the bottom of the screen you should see a bunch of commands. The caret ^ signifies holding down the control key and typing the letter.

All the list of commands are higlighted as we press ctrl key.

To quit nano, which was the biggest question I had, just use ctrl + X It prompts to save if you changed the file. press y or n and we’re done.

Here’s more from the nano editor org

Comments

Javascript vs Ruby Methods

source

ARRAYS
RubyJavascript
1
2
3
a = ["1", "2"]

a.push("3")
1
a.map!(&:to_i) # [1, 2, 3]
1
2
a.delete_at(1)
a # [1, 3]
1
a.reverse # [3, 1]
1
2
3
4
5
6
a.unshift(777)




# [777, 3, 1]
1
2
3
var a = ["1", "2"];

a.push("3");
1
a = a.map(function(n) { return parseInt(n, 10); });
1
2
a.splice(1, 1 /* how much */);
a; // [1, 3]
1
a.reverse() // [3, 1]
1
2
3
4
5
6
a.unshift(777); // 777
a; // [777, 3, 1]

/* or in place: */
var b = [3, 1];
[777].concat(b); // [777, 3, 1]
1
2
3
a = [1, 2, 3]

a.index(2) # 1 
1
a.all?{|n| n > 4} # false
1
a.any?{|n| n > 2} # true
1
a.keep_if{|n| n > 1} # [2, 3]
1
2
3
var a = [1, 2, 3];

a.indexOf(2); // 1
1
a.every(function(n) { return n > 4; }); // false
1
a.some(function(n) { return n > 2; });  // true
1
a.filter(function(n) { return n > 1;}); // [2, 3]
1
2
3
4
a = ["aaa  ", "  bbb", "  ccc  "]

a.map(&:strip)
# ["aaa", "bbb", "ccc"]
1
2
3
4
var a = ["aaa  ", "  bbb", "  ccc  "]

a.map(function(x) { return x.trim(); });               // ['aaa', 'bbb', 'ccc']
a.map(Function.prototype.call, String.prototype.trim); // ['aaa', 'bbb', 'ccc']
1
2
3
4
a = [1, 2, 3, 4, 5]

a.slice(1..-2)  # [2, 3, 4]
a[1..-2]        # [2, 3, 4]
1
2
3
4
var a = [1, 2, 3, 4, 5];


a.slice(1, -1); // [2, 3, 4]
1
2
3
4
5
6
7
a = [1, 2, 3]

a.each {|n| puts n}

a.each do |n|
    puts n
end
1
2
3
for i in 0..(a.length - 1) do
  puts a[i]
end
1
2
3
4
5
6
7
var a = [1, 2, 3];





a.forEach(function(n) { console.log(n); })
1
2
3
for (var i = 0; i < a.length; i++) {
  console.log(a[i]);
}


STRINGS
RubyJavascript
1
'hello'.index('e')    # 1
1
'hello'.rindex('l')   # 3
1
if 'hello'.include? 'lo' then puts 'found' end
1
'hello' * 3           # 'hellohellohello'
1
'a/b/c'.split('/')    # ['a', 'b', 'c']
1
'hello'.indexOf('e')             // 1
1
'hello'.lastIndexOf('l')         // 3
1
if (~'hello'.indexOf('lo')) { console.log('found'); }
1
(new Array(3 + 1)).join('hello') // 'hellohellohello'
1
'a/b/c'.split('/')               // ['a', 'b', 'c']


HASH
RubyJavascript
1
2
3
4
5
h = {}
h['a'] = 1
h['b'] = 2

h.each {|key, value| puts "#{key} #{value}" }
1
h.keys # ['a', 'b']
1
h.has_key?('c') # false
1
h.length # 2
1
h.delete("b")
1
2
3
4
5
var h = {};
h['a'] = 1;
h['b'] = 2;

for (key in h) { console.log(key, h[key]); }
1
Object.keys(h); // ['a', 'b']
1
h.hasOwnProperty('c') // false
1
Object.keys(h).length // 2
1
delete h.b


FUNCTIONS
RubyJavascript
1
2
3
4
5
6
def plus_5(num = 0) num + 5 end

plus_5     # 5
plus_5(10) # 15

[5, 10, 15].map { |k| plus_5(k) } # [10, 15, 20]
1
2
3
4
def f(*args)

  puts args
end
1
2
3
4
5
6
function plus_5(num) { return (num || 0) + 5; }

plus_5();   // 5
plus_5(10); // 15

[5, 10, 15].map(plus_5); // [10, 15, 20]
1
2
3
4
function f() {
  var args = Array.prototype.slice.call(arguments);
  console.log(args);
}


CLASSES
RubyJavascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Person
  attr_accessor :firstName, :lastName

  def initialize(firstName, lastName)
    @firstName = firstName
    @lastName = lastName
  end

  def fullName
    @firstName + " " + @lastName
  end
end

john = Person.new("John", "Malkovic")
john.fullName # "John Malkovic"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}




Person.prototype.fullName = function() {
  return this.firstName + " " + this.lastName;
}


var john = new Person("John", "Malkovic");
john.fullName(); // "John Malkovic"


MATH
RubyJavascript
1
[-5, -1, -8].max            # -1
1
[-5, 15, 20].reduce(0, &:+) # 30
1
Math.max.apply(null, [-5, -1, -8]) // -1
1
[-5, 15, 20].reduce(function(sum, value) { return sum + value; }, 0) // 30


MISC..
RubyJavascript
1
2
prng = Random.new()
prng.rand(5..9) # one of [5, 6, 7, 8, 9]
1
a, b = b, a # switch a and b
1
2
function rand(a, b) { return Math.floor(Math.random() * (b - a + 1) + a); }
rand(5, 9); // one of [5, 6, 7, 8, 9]
1
a = [b, b = a][0]
Comments

4 Ways to Call a Ruby Method

There is 4 ways to excute ruby method. Two of them can excute private method out of self class.

I’m pretty sure that you have heard lots about ruby, specially as being a dynamic language, you can create methods on the fly, add instance variables, define constants and invoke existing methods dynamically , and that’s what this post is all about :

As you know in ruby you can call a public instance method directly ,ex :

1
2
3
s= "hi man"
p s.length #=> 6
p s.include? "hi" #=> true

One way to invoke a method dynamically in ruby is to send a message to the object :

1
2
p s.send(:length) #=> 6
p s.send(:include?,"hi") #=> true

A second way is instantiate a method object and then call it:

1
2
3
4
method_object = s.method(:length)
p method_object.call #=> 6
method_object = s.method(:include?)
p method_object.call('hi')  #=> true

And the third way is to use the eval method:

1
2
eval "s.length" #=> 6
eval "s.include? 'hi'" #=>true

Well, when to use what?

look at this script, it will be used to benchmark the 3 ways of calling :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require "benchmark"
test = "hi man"
m = test.method(:length)
n = 100000
Benchmark.bmbm {|x|
  x.report("call") { n.times { m.call } }
  x.report("send") { n.times { test.send(:length) } }
  x.report("eval") { n.times { eval "test.length" } }
}
#######################################
#####   The results
#######################################
#Rehearsal ----------------------------------------
#call   0.050000   0.020000   0.070000 (  0.077915)
#send   0.080000   0.000000   0.080000 (  0.086071)
#eval   0.360000   0.040000   0.400000 (  0.405647)
#------------------------------- total: 0.550000sec

#          user     system      total        real
#call   0.050000   0.020000   0.070000 (  0.072041)
#send   0.070000   0.000000   0.070000 (  0.077674)
#eval   0.370000   0.020000   0.390000 (  0.399442)

Well as you can see, instantiating a method object is the fastest dynamic way in calling a method, also notice how slow using eval is.

Also when sending a message to an object , or when instantiating a method object , u can call private methods of that object :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Foo
  private
  def hi
    puts "hi man"
  end
end

# Normal method calling
f = Foo.new  #=> <Foo:0x10a0d51>
f.hi  #=>NoMethodError: private method `hi' called for #<Foo:0x10a0d51> 

# Sending a message
f.send :hi #  hi man

# Instantiating a method object
f.method(:hi).call  # hi man

# Using eval
eval "f.hi"  #=>NoMethodError: private method `hi' called for #<Foo:0x10a0d51> 

# Using instance_eval
f.instance_eval {hi}  # hi man

Ruby Timeout

standard timeout

1
2
3
4
5
6
7
8
require 'timeout'
begin
  complete_results = Timeout.timeout(1) do
   sleep(2)
  end
rescue Timeout::Error
  puts 'Print me something please'
end

sometime, the code inner with begin will catch exception such as:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
require 'timeout'

puts "#{Time.now}: Starting"
begin
  Timeout.timeout(5) do
    begin
      sleep 10
    rescue Exception => e
      puts "#{Time.now}: Caught an exception: #{e.inspect}"
    end
    sleep 10
  end
rescue Timeout::Error => e
  puts "#{Time.now}: Timeout: #{e}"
else
  puts "#{Time.now}: Never timed out."
end

so new a thread, as ruby 1.9 thread is native

1
2
3
4
5
6
7
begin
  complete_results = Timeout.timeout(4) do
    Thread.new{ results = platform.search(artist, album_name) }.value
  end
rescue Timeout::Error
  puts 'Print me something please'
end

implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# From lib/timeout.rb

def timeout(sec, exception=Error)
  return yield if sec == nil or sec.zero?
  raise ThreadError, "timeout within critical session" if Thread.critical
  begin
    x = Thread.current
    y = Thread.start {
      sleep sec
      x.raise exception, "execution expired" if x.alive?
    }
    yield sec
    #    return true
  ensure
    y.kill if y and y.alive?
  end
end

system timer only for 1.8.

Comments

Rspec Test Failed After Changing to Capybara-webkit

problem as

written some RSpec test for my rails 3.2 application and because I was annyoed by the Browser popping up ich tried to change from firefox to capybara-webkit. After this all tests still run, except one. The line that is failing is:

1
expect { click_button "Create" }.to change(Answer, :count).by(count)

solution is easy, The simplest way to resolve this is to wait before checking:

1
expect { click_button "Create"; sleep 2 }.to change(Answer, :count).by(count)

There is a race condition here between Capybara sending the click action to the server and your test checking the database.

Comments

Capybara Webkit or Selenium or Watir to Simulate Browser Behavior

Sometimes we want to simulate browser behavior. The situation can be test or automation script.

install capybara-webkit

1
2
3
#capybara-webkit need qt
#ubuntu
sudo aptitude install libqt4-dev

using capybara dsl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
require 'capybara'
require 'capybara/dsl'

Capybara.default_driver = :webkit

module MyModule
  include Capybara::DSL

  def login!
    within("//form[@id='session']") do
      fill_in 'Login', :with => 'user@example.com'
      fill_in 'Password', :with => 'password'
      fill_in('First Name', :with => 'John')
      fill_in('Password', :with => 'Seekrit')
      fill_in('Description', :with => 'Really Long Text...')
      choose('A Radio Button')
      check('A Checkbox')
      uncheck('A Checkbox')
      attach_file('Image', '/path/to/image.jpg')
      select('Option', :from => 'Select Box')
      end
      click_link 'Sign in'
  end
end

Debugging

It can be useful to take a snapshot of the page as it currently is and take a look at it:

1
save_and_open_page

You can also retrieve the current state of the DOM as a string using page.html.

1
print page.html

This is mostly useful for debugging. You should avoid testing against the contents of page.html and use the more expressive finder methods instead.

Finally, in drivers that support it, you can save a screenshot:

1
page.save_screenshot('screenshot.png')

Calling remote servers

Normally Capybara expects to be testing an in-process Rack application, but you can also use it to talk to a web server running anywhere on the internets, by setting app_host:

1
2
3
4
Capybara.current_driver = :selenium
Capybara.app_host = 'http://www.google.com'
...
visit('/')

Note: the default driver (:rack_test) does not support running against a remote server. With drivers that support it, you can also visit any URL directly:

1
visit('http://www.google.com')

By default Capybara will try to boot a rack application automatically. You might want to switch off Capybara’s rack server if you are running against a remote application:

1
Capybara.run_server = false

Using the sessions manually

For ultimate control, you can instantiate and use a Session manually.

1
2
3
4
5
6
7
8
require 'capybara'

session = Capybara::Session.new(:webkit, my_rack_app)
session.within("//form[@id='session']") do
  session.fill_in 'Login', :with => 'user@example.com'
  session.fill_in 'Password', :with => 'password'
end
session.click_link 'Sign in'

XPath, CSS and selectors

Capybara does not try to guess what kind of selector you are going to give it, and will always use CSS by default. If you want to use XPath, you’ll need to do:

1
2
3
within(:xpath, '//ul/li') { ... }
find(:xpath, '//ul/li').text
find(:xpath, '//li[contains(.//a[@href = "#"]/text(), "foo")]').value

Alternatively you can set the default selector to XPath:

1
2
Capybara.default_selector = :xpath
find('//ul/li').text

Capybara allows you to add custom selectors, which can be very useful if you find yourself using the same kinds of selectors very often:

1
2
3
4
5
6
7
8
9
10
11
Capybara.add_selector(:id) do
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
end

Capybara.add_selector(:row) do
  xpath { |num| ".//tbody/tr[#{num}]" }
end

Capybara.add_selector(:flash_type) do
  css { |type| "#flash.#{type}" }
end

The block given to xpath must always return an XPath expression as a String, or an XPath expression generated through the XPath gem. You can now use these selectors like this:

1
2
3
find(:id, 'post_123')
find(:row, 3)
find(:flash_type, :notice)

You can specify an optional match option which will automatically use the selector if it matches the argument:

1
2
3
4
Capybara.add_selector(:id) do
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
  match { |value| value.is_a?(Symbol) }
end

Now use it like this:

1
find(:post_123)

This :id selector is already built into Capybara by default, so you don’t need to add it yourself.

Comments