capybara-js_finders 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/Gemfile +6 -0
- data/LICENSE +22 -0
- data/README.md +142 -0
- data/Rakefile +47 -0
- data/capybara-js_finders.gemspec +42 -0
- data/lib/capybara-js_finders/capybara_extensions.rb +33 -0
- data/lib/capybara-js_finders/version.rb +5 -0
- data/lib/capybara-js_finders/xpath_extensions.rb +27 -0
- data/lib/capybara-js_finders.rb +129 -0
- data/test/test_helper.rb +8 -0
- data/test/unit/find_cell_tests/app/app.rb +13 -0
- data/test/unit/find_cell_tests/app/public/js/prototype.js +6001 -0
- data/test/unit/find_cell_tests/app/views/table.erubis +153 -0
- data/test/unit/find_cell_tests/basic_test.rb +93 -0
- metadata +162 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2011 Robert Pankowecki, Gavdi Polska
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
# Capybara JS Finders
|
2
|
+
|
3
|
+
Capybara JS Finders is a set of additional finders for capybara. Currently it only contains cell finder which allows
|
4
|
+
you to find a table cell based on column and row descriptions.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Simply add it to your Gemfile and bundle it up:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem 'capybara-js_finders', '~> 0.0.1'
|
12
|
+
gem 'capybara'
|
13
|
+
```
|
14
|
+
|
15
|
+
Make sure to add it before `capybara` in your Gemfile!
|
16
|
+
|
17
|
+
## API
|
18
|
+
|
19
|
+
Use it like any other capybara finder.
|
20
|
+
|
21
|
+
### find_cell
|
22
|
+
|
23
|
+
Allows you to find table cell (td, th) based on cell and row descriptions.
|
24
|
+
The method is colspan and rowspan attribute-aware which means it will be able to find
|
25
|
+
a cell even if it is under collspaned th containing a description.
|
26
|
+
|
27
|
+
#### Example
|
28
|
+
|
29
|
+
<table>
|
30
|
+
<tr>
|
31
|
+
<th>
|
32
|
+
User
|
33
|
+
</th>
|
34
|
+
<th>
|
35
|
+
Email
|
36
|
+
</th>
|
37
|
+
<th>
|
38
|
+
Permissions
|
39
|
+
</th>
|
40
|
+
</tr>
|
41
|
+
|
42
|
+
<tr>
|
43
|
+
<td>
|
44
|
+
John Smith
|
45
|
+
</td>
|
46
|
+
<td>
|
47
|
+
john@example.org
|
48
|
+
</td>
|
49
|
+
<td>
|
50
|
+
Admin
|
51
|
+
</td>
|
52
|
+
</tr>
|
53
|
+
|
54
|
+
<tr>
|
55
|
+
<td>
|
56
|
+
Andrew Bon
|
57
|
+
</td>
|
58
|
+
<td>
|
59
|
+
andrew@example.org
|
60
|
+
</td>
|
61
|
+
<td>
|
62
|
+
Moderator
|
63
|
+
</td>
|
64
|
+
</tr>
|
65
|
+
|
66
|
+
</table>
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
assert find_cell(:row => "John Smith", :column => "Permissions").has_content?("Admin")
|
70
|
+
assert find_cell(:row => "Andrew Bon", :column => "Email").has_no_content?("john")
|
71
|
+
```
|
72
|
+
|
73
|
+
#### Example
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
assert find_cell(:row => "John Smith", :column => "January", :text => "28").has_text?("Present at work")
|
77
|
+
```
|
78
|
+
|
79
|
+
#### Multicolumn and multirows support
|
80
|
+
|
81
|
+
If there are many rows and/or columns matching `:row` and/or `:column` parameter you can wider the search to include all of them
|
82
|
+
by using `:multirow` and/or `:multicolumn` action.
|
83
|
+
|
84
|
+
##### Example
|
85
|
+
|
86
|
+
<table>
|
87
|
+
<tr>
|
88
|
+
<th>
|
89
|
+
User
|
90
|
+
</th>
|
91
|
+
<th>
|
92
|
+
Email
|
93
|
+
</th>
|
94
|
+
<th>
|
95
|
+
Permissions
|
96
|
+
</th>
|
97
|
+
</tr>
|
98
|
+
|
99
|
+
<tr>
|
100
|
+
<td>
|
101
|
+
John Smith
|
102
|
+
</td>
|
103
|
+
<td>
|
104
|
+
john@example.org
|
105
|
+
</td>
|
106
|
+
<td>
|
107
|
+
Admin
|
108
|
+
</td>
|
109
|
+
</tr>
|
110
|
+
|
111
|
+
<tr>
|
112
|
+
<td>
|
113
|
+
John Smith
|
114
|
+
</td>
|
115
|
+
<td>
|
116
|
+
smith@example.org
|
117
|
+
</td>
|
118
|
+
<td>
|
119
|
+
Moderator
|
120
|
+
</td>
|
121
|
+
</tr>
|
122
|
+
|
123
|
+
</table>
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
find_cell(:row => "John Smith", :column => "Permissions", :text => "Moderator") # raises an exception
|
127
|
+
find_cell(:multirow => true, :row => "John Smith", :column => "Permissions", :text => "Moderator") # will find the proper cell
|
128
|
+
```
|
129
|
+
|
130
|
+
## License
|
131
|
+
|
132
|
+
MIT License
|
133
|
+
|
134
|
+
## Integration
|
135
|
+
|
136
|
+
Integrates nicely with bbq
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
user = Bbq::TestUser.new(:driver => :selenium, :session_name => :default)
|
140
|
+
user.visit '/page'
|
141
|
+
assert user.find_cell(:row => "RowDescription", :column => "ColumnDescription").has_content?("CellContent")
|
142
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
Bundler::GemHelper.install_tasks
|
9
|
+
require 'rdoc/task'
|
10
|
+
require 'rake/testtask'
|
11
|
+
require 'rake'
|
12
|
+
|
13
|
+
RDoc::Task.new do |rdoc|
|
14
|
+
rdoc.rdoc_dir = 'rdoc'
|
15
|
+
rdoc.title = 'Capybara JS Finders'
|
16
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
17
|
+
rdoc.rdoc_files.include('README.rdoc')
|
18
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
19
|
+
end
|
20
|
+
|
21
|
+
Rake::TestTask.new(:test) do |t|
|
22
|
+
t.libs << 'lib'
|
23
|
+
t.libs << 'test'
|
24
|
+
t.pattern = 'test/**/*_test.rb'
|
25
|
+
t.verbose = false
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
task :default => :test
|
30
|
+
|
31
|
+
task :script do
|
32
|
+
require "capybara-js_finders"
|
33
|
+
puts Capybara::JsFinders::SCRIPT
|
34
|
+
end
|
35
|
+
|
36
|
+
namespace :find_cell_tests do
|
37
|
+
task :app do
|
38
|
+
$LOAD_PATH << './test'
|
39
|
+
require 'unit/find_cell_tests/app/app'
|
40
|
+
FindCellTests::App.run!
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
task :readme do
|
45
|
+
#require 'redcloth'
|
46
|
+
`redcarpet README.md > README.test.html`
|
47
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "capybara-js_finders/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "capybara-js_finders"
|
7
|
+
s.version = Capybara::JsFinders::VERSION
|
8
|
+
s.authors = ["Robert Pankowecki"]
|
9
|
+
s.email = %w(robert.pankowecki@gmail.com rpa@gavdi.com)
|
10
|
+
s.homepage = "https://github.com/paneq/capybara-js_finders"
|
11
|
+
s.summary = %q{Additional finders for capybara that for some reason cannot use only xpath for finding nodes but needs to execute js for some calculations}
|
12
|
+
s.description = <<-DESC
|
13
|
+
Additional finders for capybara that for some reason
|
14
|
+
cannot use only xpath for finding nodes but needs to
|
15
|
+
execute js for some calculations.
|
16
|
+
|
17
|
+
Ex: I you want to find a table cell
|
18
|
+
that is under or next to other cell the easiest way to do it is to
|
19
|
+
check their position on page and compare them. This way you do not
|
20
|
+
need to wory about calculating the effects of using colspan
|
21
|
+
and rowspan attributes.
|
22
|
+
|
23
|
+
The downside is that you need to use capybara driver that runs
|
24
|
+
a browser like selenium.
|
25
|
+
DESC
|
26
|
+
|
27
|
+
s.rubyforge_project = "capybara-js_finders"
|
28
|
+
|
29
|
+
s.files = `git ls-files`.split("\n")
|
30
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
31
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
|
34
|
+
s.add_dependency "capybara", "~> 1.0.0"
|
35
|
+
s.add_development_dependency "rake", "~> 0.9.0"
|
36
|
+
s.add_development_dependency "rdoc", ">= 2.4.2"
|
37
|
+
s.add_development_dependency "sinatra", "~> 1.2.6"
|
38
|
+
s.add_development_dependency "erubis", "~> 2.6.6"
|
39
|
+
s.add_development_dependency "bbq", "~> 0.0.3"
|
40
|
+
s.add_development_dependency "redcarpet", "~> 1.17"
|
41
|
+
s.add_development_dependency "ruby-debug19"
|
42
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'capybara/node/finders'
|
2
|
+
|
3
|
+
Capybara::Node::Finders.send(:include, Capybara::JsFinders)
|
4
|
+
|
5
|
+
require 'capybara/dsl'
|
6
|
+
|
7
|
+
Capybara::DSL.module_eval do
|
8
|
+
def execute_script(*args, &block)
|
9
|
+
page.execute_script(*args, &block)
|
10
|
+
end
|
11
|
+
def find_cell(*params, &block)
|
12
|
+
page.find_cell(*params, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'capybara'
|
17
|
+
|
18
|
+
Capybara::Node::Base.module_eval do
|
19
|
+
def execute_script(script)
|
20
|
+
driver.execute_script(script)
|
21
|
+
end
|
22
|
+
def evaluate_script(script)
|
23
|
+
driver.evaluate_script(script)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
Capybara::Session::NODE_METHODS << :find_cell
|
28
|
+
Capybara::Session::DSL_METHODS << :find_cell
|
29
|
+
Capybara::Session.class_eval do
|
30
|
+
def find_cell(*params, &block)
|
31
|
+
current_node.find_cell(*params, &block)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'xpath'
|
2
|
+
|
3
|
+
module XPath
|
4
|
+
class Expression
|
5
|
+
class Father < Multiple
|
6
|
+
def to_xpath(predicate=nil)
|
7
|
+
if @expressions.length == 1
|
8
|
+
"#{@left.to_xpath(predicate)}/../self::#{@expressions.first.to_xpath(predicate)}"
|
9
|
+
elsif @expressions.length > 1
|
10
|
+
"#{@left.to_xpath(predicate)}/../self::*[#{@expressions.map { |e| "self::#{e.to_xpath(predicate)}" }.join(" | ")}]"
|
11
|
+
else
|
12
|
+
"#{@left.to_xpath(predicate)}/../self::*"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def father(*expressions)
|
19
|
+
Expression::Father.new(current, expressions)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
XPath::HTML.module_eval do
|
24
|
+
def cell(locator)
|
25
|
+
descendant(:td, :th)[attr(:id).equals(locator) | string.n.is(locator)]
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "capybara-js_finders/version"
|
2
|
+
require "capybara-js_finders/xpath_extensions"
|
3
|
+
|
4
|
+
module Capybara
|
5
|
+
|
6
|
+
module JsFinders
|
7
|
+
|
8
|
+
extend XPath
|
9
|
+
|
10
|
+
LR = "lr".freeze # Left range
|
11
|
+
RR = "rr".freeze # Right range
|
12
|
+
TR = "tr".freeze # Top range
|
13
|
+
BR = "br".freeze # Bottom range
|
14
|
+
XpathTrue = "(1=1)"
|
15
|
+
|
16
|
+
# TODO: This script is only prototype compatibile. Let it discover jquery or prototype and use proper methods.
|
17
|
+
SCRIPT = <<-JS
|
18
|
+
xpathResult = document.evaluate( "#{descendant(:th, :td)}", document, null, XPathResult.ANY_TYPE, null);
|
19
|
+
var ary = [];
|
20
|
+
var td;
|
21
|
+
while( td = xpathResult.iterateNext() ){
|
22
|
+
ary.push(td);
|
23
|
+
}
|
24
|
+
ary.each(function(ele){
|
25
|
+
var offset = ele.cumulativeOffset();
|
26
|
+
var lr = offset.left;
|
27
|
+
var rr = lr + ele.getWidth();
|
28
|
+
var tr = offset.top;
|
29
|
+
var br = tr + ele.getHeight();
|
30
|
+
ele.setAttribute('#{LR}', lr);
|
31
|
+
ele.setAttribute('#{RR}', rr);
|
32
|
+
ele.setAttribute('#{TR}', tr);
|
33
|
+
ele.setAttribute('#{BR}', br);
|
34
|
+
});
|
35
|
+
JS
|
36
|
+
SCRIPT.freeze
|
37
|
+
|
38
|
+
def self.overlaps_columns(columns)
|
39
|
+
return XpathTrue if columns.first == true
|
40
|
+
columns_xpath = columns.map{|column| overlaps_column(column) }.join(" or ")
|
41
|
+
columns_xpath = "( #{columns_xpath} )"
|
42
|
+
return columns_xpath
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.overlaps_column(column)
|
46
|
+
overlaps_horizontal(column[LR], column[RR])
|
47
|
+
end
|
48
|
+
|
49
|
+
# clr, crr - Column left and right range
|
50
|
+
def self.overlaps_horizontal(clr, crr)
|
51
|
+
"(./@#{RR} >= #{clr} and ./@#{LR} <= #{crr})"
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.overlaps_rows(rows)
|
55
|
+
return XpathTrue if rows.first == true
|
56
|
+
rows_xpath = rows.map{|row| overlaps_row(row) }.join(" or ")
|
57
|
+
rows_xpath = "( #{rows_xpath} )"
|
58
|
+
return rows_xpath
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.overlaps_row(row)
|
62
|
+
overlaps_vertical(row[TR], row[BR])
|
63
|
+
end
|
64
|
+
|
65
|
+
# rtr, rbr - row top and bottom range
|
66
|
+
def self.overlaps_vertical(rtr, rbr)
|
67
|
+
"(./@#{BR} >= #{rtr} and ./@#{TR} <= #{rbr})"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Condition for td to be under one of column and on the same level as one of the rows
|
71
|
+
def self.cell_condition(columns, rows)
|
72
|
+
xpath = "( #{overlaps_rows(rows)} ) and ( #{overlaps_columns(columns)} ) "
|
73
|
+
return xpath
|
74
|
+
end
|
75
|
+
|
76
|
+
# :column - text which should appear in the table column, or td/th Capybara::Node::Element object [or Array of them]
|
77
|
+
# representing the columns which narrow the search area of the table. Nil is allowed.
|
78
|
+
#
|
79
|
+
# :row - text which should appear in the row of the table, or td/th Capybara::Node::Element object [or Array of them]
|
80
|
+
# representing the columns which narrow the search area of the table. Nil is allowed.
|
81
|
+
# :text - text which should appear in the cell
|
82
|
+
# :multicolumn - set to true if finding column by test should return multiple results instead of just one
|
83
|
+
# :multirow - set to true if finding rows by test should return multiple results instead of just one
|
84
|
+
#
|
85
|
+
# :visible
|
86
|
+
# :with
|
87
|
+
def find_cell(options = {})
|
88
|
+
# TODO: Jak pierwszy arg to string to wywolaj cell zamiast tego
|
89
|
+
|
90
|
+
columns = case options[:column]
|
91
|
+
when String
|
92
|
+
method = options[:multicolumn] ? :all : :find
|
93
|
+
send(method, :xpath, XPath::HTML.cell(options[:column]) ) # TODO: jak all to musi byc conajmniej 1
|
94
|
+
when Capybara::Node::Element
|
95
|
+
options[:column]
|
96
|
+
when Array
|
97
|
+
options[:column].each{|x| raise ArgumentError unless x.is_a?(Capybara::Node::Element) }
|
98
|
+
when NilClass
|
99
|
+
true
|
100
|
+
else
|
101
|
+
raise ArgumentError
|
102
|
+
end
|
103
|
+
columns = Array.wrap(columns)
|
104
|
+
|
105
|
+
rows = case options[:row]
|
106
|
+
when String
|
107
|
+
method = options[:multirow] ? :all : :find
|
108
|
+
send(method, :xpath, XPath::HTML.cell(options[:row]) ) # TODO: jak all to musi byc conajmniej 1
|
109
|
+
when Capybara::Node::Element
|
110
|
+
options[:row]
|
111
|
+
when Array
|
112
|
+
options[:row].each{|x| raise ArgumentError unless x.is_a?(Capybara::Node::Element) }
|
113
|
+
when NilClass
|
114
|
+
true
|
115
|
+
else
|
116
|
+
raise ArgumentError
|
117
|
+
end
|
118
|
+
rows = Array.wrap(rows)
|
119
|
+
|
120
|
+
execute_script(SCRIPT)
|
121
|
+
xpath = JsFinders.cell_condition(columns, rows)
|
122
|
+
xpath = ".//td[ #{xpath} ]"
|
123
|
+
#puts xpath
|
124
|
+
find(:xpath, xpath, options)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
require "capybara-js_finders/capybara_extensions"
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module FindCellTests
|
4
|
+
|
5
|
+
class App < Sinatra::Base
|
6
|
+
set :views, File.join( File.expand_path( File.dirname(__FILE__) ), 'views' )
|
7
|
+
set :public, File.join( File.expand_path( File.dirname(__FILE__) ), 'public' )
|
8
|
+
get "/" do
|
9
|
+
erubis :table
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|