capybara 1.1.4 → 2.0.0.beta2

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.
Files changed (84) hide show
  1. data/History.txt +100 -0
  2. data/License.txt +22 -0
  3. data/README.md +829 -0
  4. data/lib/capybara.rb +124 -6
  5. data/lib/capybara/cucumber.rb +2 -5
  6. data/lib/capybara/driver/base.rb +5 -5
  7. data/lib/capybara/driver/node.rb +2 -2
  8. data/lib/capybara/dsl.rb +3 -121
  9. data/lib/capybara/node/actions.rb +12 -28
  10. data/lib/capybara/node/base.rb +5 -13
  11. data/lib/capybara/node/element.rb +21 -21
  12. data/lib/capybara/node/finders.rb +27 -89
  13. data/lib/capybara/node/matchers.rb +107 -69
  14. data/lib/capybara/node/simple.rb +11 -13
  15. data/lib/capybara/query.rb +78 -0
  16. data/lib/capybara/rack_test/browser.rb +16 -27
  17. data/lib/capybara/rack_test/driver.rb +11 -1
  18. data/lib/capybara/rack_test/node.rb +17 -1
  19. data/lib/capybara/result.rb +84 -0
  20. data/lib/capybara/rspec/matchers.rb +28 -63
  21. data/lib/capybara/selector.rb +97 -33
  22. data/lib/capybara/selenium/driver.rb +14 -61
  23. data/lib/capybara/selenium/node.rb +6 -15
  24. data/lib/capybara/server.rb +32 -27
  25. data/lib/capybara/session.rb +54 -30
  26. data/lib/capybara/spec/public/jquery-ui.js +791 -0
  27. data/lib/capybara/spec/public/jquery.js +9046 -0
  28. data/lib/capybara/spec/public/test.js +4 -1
  29. data/lib/capybara/spec/session.rb +56 -27
  30. data/lib/capybara/spec/session/all_spec.rb +8 -4
  31. data/lib/capybara/spec/session/attach_file_spec.rb +12 -9
  32. data/lib/capybara/spec/session/check_spec.rb +6 -3
  33. data/lib/capybara/spec/session/choose_spec.rb +4 -1
  34. data/lib/capybara/spec/session/click_button_spec.rb +5 -14
  35. data/lib/capybara/spec/session/click_link_or_button_spec.rb +2 -1
  36. data/lib/capybara/spec/session/click_link_spec.rb +3 -17
  37. data/lib/capybara/spec/session/current_url_spec.rb +77 -9
  38. data/lib/capybara/spec/session/fill_in_spec.rb +8 -18
  39. data/lib/capybara/spec/session/find_spec.rb +19 -46
  40. data/lib/capybara/spec/session/first_spec.rb +2 -34
  41. data/lib/capybara/spec/session/has_css_spec.rb +1 -1
  42. data/lib/capybara/spec/session/has_field_spec.rb +28 -0
  43. data/lib/capybara/spec/session/has_select_spec.rb +84 -31
  44. data/lib/capybara/spec/session/has_table_spec.rb +7 -69
  45. data/lib/capybara/spec/session/has_text_spec.rb +168 -0
  46. data/lib/capybara/spec/session/javascript.rb +65 -81
  47. data/lib/capybara/spec/session/node_spec.rb +115 -0
  48. data/lib/capybara/spec/session/screenshot.rb +29 -0
  49. data/lib/capybara/spec/session/select_spec.rb +12 -12
  50. data/lib/capybara/spec/session/text_spec.rb +9 -4
  51. data/lib/capybara/spec/session/unselect_spec.rb +12 -6
  52. data/lib/capybara/spec/session/visit_spec.rb +76 -0
  53. data/lib/capybara/spec/session/within_frame_spec.rb +33 -0
  54. data/lib/capybara/spec/session/within_spec.rb +47 -58
  55. data/lib/capybara/spec/session/within_window_spec.rb +40 -0
  56. data/lib/capybara/spec/test_app.rb +27 -3
  57. data/lib/capybara/spec/views/form.erb +11 -10
  58. data/lib/capybara/spec/views/host_links.erb +2 -2
  59. data/lib/capybara/spec/views/tables.erb +6 -66
  60. data/lib/capybara/spec/views/with_html.erb +3 -3
  61. data/lib/capybara/spec/views/with_js.erb +11 -8
  62. data/lib/capybara/util/save_and_open_page.rb +4 -3
  63. data/lib/capybara/version.rb +1 -1
  64. data/spec/basic_node_spec.rb +15 -3
  65. data/spec/dsl_spec.rb +12 -10
  66. data/spec/rack_test_spec.rb +152 -0
  67. data/spec/rspec/features_spec.rb +0 -2
  68. data/spec/rspec/matchers_spec.rb +164 -89
  69. data/spec/rspec_spec.rb +0 -2
  70. data/spec/selenium_spec.rb +67 -0
  71. data/spec/server_spec.rb +35 -23
  72. data/spec/spec_helper.rb +18 -2
  73. metadata +30 -30
  74. data/README.rdoc +0 -722
  75. data/lib/capybara/spec/driver.rb +0 -301
  76. data/lib/capybara/spec/session/current_host_spec.rb +0 -68
  77. data/lib/capybara/spec/session/has_content_spec.rb +0 -106
  78. data/lib/capybara/util/timeout.rb +0 -27
  79. data/spec/driver/rack_test_driver_spec.rb +0 -89
  80. data/spec/driver/selenium_driver_spec.rb +0 -37
  81. data/spec/session/rack_test_session_spec.rb +0 -55
  82. data/spec/session/selenium_session_spec.rb +0 -26
  83. data/spec/string_spec.rb +0 -77
  84. data/spec/timeout_spec.rb +0 -28
@@ -95,7 +95,7 @@ module Capybara
95
95
  # @return [Boolean] Whether the element is visible
96
96
  #
97
97
  def visible?
98
- native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none')]").size == 0
98
+ native.xpath("./ancestor-or-self::*[contains(@style, 'display:none') or contains(@style, 'display: none') or name()='script' or name()='head']").size == 0
99
99
  end
100
100
 
101
101
  ##
@@ -118,22 +118,20 @@ module Capybara
118
118
  native[:selected]
119
119
  end
120
120
 
121
- def allow_reload!
122
- # no op
123
- end
124
-
125
- def without_wait
126
- yield
121
+ def synchronize
122
+ yield # simple nodes don't need to wait
127
123
  end
128
124
 
129
- protected
130
-
131
- def find_in_base(selector, xpath)
132
- native.xpath(xpath).map { |node| self.class.new(node) }
125
+ def allow_reload!
126
+ # no op
133
127
  end
134
128
 
135
- def wait_until
136
- yield # simple nodes don't need to wait
129
+ def all(*args)
130
+ query = Capybara::Query.new(*args)
131
+ elements = native.xpath(query.xpath).map do |node|
132
+ self.class.new(node)
133
+ end
134
+ Capybara::Result.new(elements, query)
137
135
  end
138
136
  end
139
137
  end
@@ -0,0 +1,78 @@
1
+ module Capybara
2
+ class Query
3
+ attr_accessor :selector, :locator, :options, :xpath, :find, :negative
4
+
5
+ VALID_KEYS = [:text, :visible, :between, :count, :maximum, :minimum]
6
+
7
+ def initialize(*args)
8
+ @options = if args.last.is_a?(Hash) then args.pop.dup else {} end
9
+
10
+ unless options.has_key?(:visible)
11
+ @options[:visible] = Capybara.ignore_hidden_elements
12
+ end
13
+
14
+ if args[1]
15
+ @selector = Selector.all[args[0]]
16
+ @locator = args[1]
17
+ else
18
+ @selector = Selector.all.values.find { |s| s.match?(args[0]) }
19
+ @locator = args[0]
20
+ end
21
+ @selector ||= Selector.all[Capybara.default_selector]
22
+
23
+ @xpath = @selector.call(@locator).to_s
24
+
25
+ assert_valid_keys!
26
+ end
27
+
28
+ def name; selector.name; end
29
+ def label; selector.label or selector.name; end
30
+
31
+ def description
32
+ @description = "#{label} #{locator.inspect}"
33
+ @description << " with text #{options[:text].inspect}" if options[:text]
34
+ @description
35
+ end
36
+
37
+ def matches_filters?(node)
38
+ if options[:text]
39
+ regexp = options[:text].is_a?(Regexp) ? options[:text] : Regexp.escape(options[:text])
40
+ return false if not node.text.match(regexp)
41
+ end
42
+ return false if options[:visible] and not node.visible?
43
+ selector.custom_filters.each do |name, block|
44
+ return false if options.has_key?(name) and not block.call(node, options[name])
45
+ end
46
+ true
47
+ end
48
+
49
+ def matches_count?(count)
50
+ case
51
+ when count.zero?
52
+ false
53
+ when options[:between]
54
+ options[:between] === count
55
+ when options[:count]
56
+ options[:count].to_i == count
57
+ when options[:maximum]
58
+ options[:maximum].to_i >= count
59
+ when options[:minimum]
60
+ options[:minimum].to_i <= count
61
+ else
62
+ count > 0
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def assert_valid_keys!
69
+ valid_keys = VALID_KEYS + @selector.custom_filters.keys
70
+ invalid_keys = @options.keys - valid_keys
71
+ unless invalid_keys.empty?
72
+ invalid_names = invalid_keys.map(&:inspect).join(", ")
73
+ valid_names = valid_keys.map(&:inspect).join(", ")
74
+ raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -18,35 +18,36 @@ class Capybara::RackTest::Browser
18
18
 
19
19
  def visit(path, attributes = {})
20
20
  reset_host!
21
- process(:get, path, attributes)
22
- follow_redirects!
21
+ process_and_follow_redirects(:get, path, attributes)
23
22
  end
24
23
 
25
24
  def submit(method, path, attributes)
26
25
  path = request_path if not path or path.empty?
27
- process(method, path, attributes)
28
- follow_redirects!
26
+ process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
29
27
  end
30
28
 
31
29
  def follow(method, path, attributes = {})
32
30
  return if path.gsub(/^#{request_path}/, '').start_with?('#')
33
- process(method, path, attributes)
34
- follow_redirects!
31
+ process_and_follow_redirects(method, path, attributes, {'HTTP_REFERER' => current_url})
35
32
  end
36
33
 
37
- def follow_redirects!
38
- 5.times do
39
- process(:get, last_response["Location"]) if last_response.redirect?
34
+ def process_and_follow_redirects(method, path, attributes = {}, env = {})
35
+ process(method, path, attributes, env)
36
+ if driver.follow_redirects?
37
+ driver.redirect_limit.times do
38
+ process(:get, last_response["Location"], {}, env) if last_response.redirect?
39
+ end
40
+ raise Capybara::InfiniteRedirectError, "redirected more than #{driver.redirect_limit} times, check for infinite redirects." if last_response.redirect?
40
41
  end
41
- raise Capybara::InfiniteRedirectError, "redirected more than 5 times, check for infinite redirects." if last_response.redirect?
42
42
  end
43
43
 
44
- def process(method, path, attributes = {})
44
+ def process(method, path, attributes = {}, env = {})
45
45
  new_uri = URI.parse(path)
46
- current_uri = URI.parse(current_url)
46
+ method.downcase! unless method.is_a? Symbol
47
47
 
48
48
  if new_uri.host
49
- @current_host = new_uri.scheme + '://' + new_uri.host
49
+ @current_host = "#{new_uri.scheme}://#{new_uri.host}"
50
+ @current_host << ":#{new_uri.port}" if new_uri.port != new_uri.default_port
50
51
  end
51
52
 
52
53
  if new_uri.relative?
@@ -59,7 +60,7 @@ class Capybara::RackTest::Browser
59
60
  end
60
61
 
61
62
  reset_cache!
62
- send(method, path, attributes, env)
63
+ send(method, path, attributes, env.merge(options[:headers] || {}))
63
64
  end
64
65
 
65
66
  def current_url
@@ -91,7 +92,7 @@ class Capybara::RackTest::Browser
91
92
  def source
92
93
  last_response.body
93
94
  rescue Rack::Test::Error
94
- nil
95
+ ""
95
96
  end
96
97
 
97
98
  protected
@@ -106,16 +107,4 @@ protected
106
107
  rescue Rack::Test::Error
107
108
  ""
108
109
  end
109
-
110
- def env
111
- env = {}
112
- begin
113
- env["HTTP_REFERER"] = last_request.url
114
- rescue Rack::Test::Error
115
- # no request yet
116
- end
117
- env.merge!(options[:headers]) if options[:headers]
118
- env
119
- end
120
-
121
110
  end
@@ -6,7 +6,9 @@ require 'cgi'
6
6
 
7
7
  class Capybara::RackTest::Driver < Capybara::Driver::Base
8
8
  DEFAULT_OPTIONS = {
9
- :respect_data_method => true
9
+ :respect_data_method => false,
10
+ :follow_redirects => true,
11
+ :redirect_limit => 5
10
12
  }
11
13
  attr_reader :app, :options
12
14
 
@@ -20,6 +22,14 @@ class Capybara::RackTest::Driver < Capybara::Driver::Base
20
22
  @browser ||= Capybara::RackTest::Browser.new(self)
21
23
  end
22
24
 
25
+ def follow_redirects?
26
+ @options[:follow_redirects]
27
+ end
28
+
29
+ def redirect_limit
30
+ @options[:redirect_limit]
31
+ end
32
+
23
33
  def response
24
34
  browser.last_response
25
35
  end
@@ -1,6 +1,6 @@
1
1
  class Capybara::RackTest::Node < Capybara::Driver::Node
2
2
  def text
3
- native.text
3
+ unnormalized_text.strip.gsub(/\s+/, ' ')
4
4
  end
5
5
 
6
6
  def [](name)
@@ -84,6 +84,22 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
84
84
  native.xpath(locator).map { |n| self.class.new(driver, n) }
85
85
  end
86
86
 
87
+ protected
88
+
89
+ def unnormalized_text
90
+ if !visible?
91
+ ''
92
+ elsif native.text?
93
+ native.text
94
+ elsif native.element?
95
+ native.children.map do |child|
96
+ Capybara::RackTest::Node.new(driver, child).unnormalized_text
97
+ end.join
98
+ else
99
+ ''
100
+ end
101
+ end
102
+
87
103
  private
88
104
 
89
105
  def string_node
@@ -0,0 +1,84 @@
1
+ module Capybara
2
+ class Result
3
+ include Enumerable
4
+
5
+ def initialize(elements, query)
6
+ @elements = elements
7
+ @result = elements.select { |node| query.matches_filters?(node) }
8
+ @rest = @elements - @result
9
+ @query = query
10
+ end
11
+
12
+ def each(&block)
13
+ @result.each(&block)
14
+ end
15
+
16
+ def first
17
+ @result.first
18
+ end
19
+
20
+ def matches_count?
21
+ @query.matches_count?(@result.size)
22
+ end
23
+
24
+ def find!
25
+ raise find_error if @result.count != 1
26
+ @result.first
27
+ end
28
+
29
+ def size; @result.size; end
30
+ alias_method :length, :size
31
+ alias_method :count, :size
32
+
33
+ def find_error
34
+ if @result.count == 0
35
+ Capybara::ElementNotFound.new("Unable to find #{@query.description}")
36
+ elsif @result.count > 1
37
+ Capybara::Ambiguous.new("Ambiguous match, found #{size} elements matching #{@query.description}")
38
+ end
39
+ end
40
+
41
+ def failure_message
42
+ message = if @query.options[:count]
43
+ "expected #{@query.description} to be found #{@query.options[:count]} #{declension("time", "times", @query.options[:count])}"
44
+ elsif @query.options[:between]
45
+ "expected #{@query.description} to be found between #{@query.options[:between].first} and #{@query.options[:between].last} times"
46
+ elsif @query.options[:maximum]
47
+ "expected #{@query.description} to be found at most #{@query.options[:maximum]} #{declension("time", "times", @query.options[:maximum])}"
48
+ elsif @query.options[:minimum]
49
+ "expected #{@query.description} to be found at least #{@query.options[:minimum]} #{declension("time", "times", @query.options[:minimum])}"
50
+ else
51
+ "expected to find #{@query.description}"
52
+ end
53
+ if count > 0
54
+ message << ", found #{count} #{declension("match", "matches")}: " << @result.map(&:text).map(&:inspect).join(", ")
55
+ else
56
+ message << " but there were no matches"
57
+ end
58
+ unless @rest.empty?
59
+ elements = @rest.map(&:text).map(&:inspect).join(", ")
60
+ message << ". Also found " << elements << ", which matched the selector but not all filters."
61
+ end
62
+ message
63
+ end
64
+
65
+ def negative_failure_message
66
+ "expected not to find #{@query.description}, but there #{declension("was", "were")} #{count} #{declension("match", "matches")}"
67
+ end
68
+
69
+ def empty?
70
+ @result.empty?
71
+ end
72
+ def [](key); @result[key]; end
73
+
74
+ private
75
+
76
+ def declension(singular, plural, count=count)
77
+ if count == 1
78
+ singular
79
+ else
80
+ plural
81
+ end
82
+ end
83
+ end
84
+ end
@@ -6,35 +6,15 @@ module Capybara
6
6
  end
7
7
 
8
8
  def matches?(actual)
9
- @actual = wrap(actual)
10
- @actual.has_selector?(*@args)
9
+ wrap(actual).assert_selector(*@args)
11
10
  end
12
11
 
13
12
  def does_not_match?(actual)
14
- @actual = wrap(actual)
15
- @actual.has_no_selector?(*@args)
16
- end
17
-
18
- def failure_message_for_should
19
- if normalized.failure_message
20
- normalized.failure_message.call(@actual, normalized)
21
- else
22
- "expected #{selector_name} to return something"
23
- end
24
- end
25
-
26
- def failure_message_for_should_not
27
- "expected #{selector_name} not to return anything"
13
+ wrap(actual).assert_no_selector(*@args)
28
14
  end
29
15
 
30
16
  def description
31
- "has #{selector_name}"
32
- end
33
-
34
- def selector_name
35
- name = "#{normalized.name} #{normalized.locator.inspect}"
36
- name << " with text #{normalized.options[:text].inspect}" if normalized.options[:text]
37
- name
17
+ "have #{query.description}"
38
18
  end
39
19
 
40
20
  def wrap(actual)
@@ -45,55 +25,38 @@ module Capybara
45
25
  end
46
26
  end
47
27
 
48
- def normalized
49
- @normalized ||= Capybara::Selector.normalize(*@args)
28
+ def query
29
+ @query ||= Capybara::Query.new(*@args)
50
30
  end
51
31
  end
52
32
 
53
- class HaveMatcher
54
- attr_reader :name, :locator, :options, :failure_message, :actual
55
-
56
- def initialize(name, locator, options={}, &block)
57
- @name = name
58
- @locator = locator
59
- @options = options
60
- @failure_message = block
61
- end
33
+ class HaveText
34
+ attr_reader :text
62
35
 
63
- def arguments
64
- if options.empty? then [locator] else [locator, options] end
36
+ def initialize(text)
37
+ @text = text
65
38
  end
66
39
 
67
40
  def matches?(actual)
68
41
  @actual = wrap(actual)
69
- @actual.send(:"has_#{name}?", *arguments)
42
+ @actual.has_text?(text)
70
43
  end
71
44
 
72
45
  def does_not_match?(actual)
73
46
  @actual = wrap(actual)
74
- @actual.send(:"has_no_#{name}?", *arguments)
47
+ @actual.has_no_text?(text)
75
48
  end
76
49
 
77
50
  def failure_message_for_should
78
- if failure_message
79
- failure_message.call(actual, self)
80
- else
81
- "expected #{selector_name} to return something"
82
- end
51
+ "expected there to be text #{text.inspect} in #{@actual.text.inspect}"
83
52
  end
84
53
 
85
54
  def failure_message_for_should_not
86
- "expected #{selector_name} not to return anything"
55
+ "expected there not to be text #{text.inspect} in #{@actual.text.inspect}"
87
56
  end
88
57
 
89
58
  def description
90
- "has #{selector_name}"
91
- end
92
-
93
- def selector_name
94
- selector_name = "#{name} #{locator.inspect}"
95
- selector_name << " with text #{options[:text].inspect}" if options[:text]
96
- selector_name
59
+ "have text #{text.inspect}"
97
60
  end
98
61
 
99
62
  def wrap(actual)
@@ -110,45 +73,47 @@ module Capybara
110
73
  end
111
74
 
112
75
  def have_xpath(xpath, options={})
113
- HaveMatcher.new(:xpath, xpath, options)
76
+ HaveSelector.new(:xpath, xpath, options)
114
77
  end
115
78
 
116
79
  def have_css(css, options={})
117
- HaveMatcher.new(:css, css, options)
80
+ HaveSelector.new(:css, css, options)
118
81
  end
119
82
 
120
83
  def have_content(text)
121
- HaveMatcher.new(:content, text.to_s) do |page, matcher|
122
- %(expected there to be content #{matcher.locator.inspect} in #{page.text.inspect})
123
- end
84
+ HaveText.new(text)
85
+ end
86
+
87
+ def have_text(text)
88
+ HaveText.new(text)
124
89
  end
125
90
 
126
91
  def have_link(locator, options={})
127
- HaveMatcher.new(:link, locator, options)
92
+ HaveSelector.new(:link, locator, options)
128
93
  end
129
94
 
130
95
  def have_button(locator)
131
- HaveMatcher.new(:button, locator)
96
+ HaveSelector.new(:button, locator)
132
97
  end
133
98
 
134
99
  def have_field(locator, options={})
135
- HaveMatcher.new(:field, locator, options)
100
+ HaveSelector.new(:field, locator, options)
136
101
  end
137
102
 
138
103
  def have_checked_field(locator)
139
- HaveMatcher.new(:checked_field, locator)
104
+ HaveSelector.new(:field, locator, :checked => true)
140
105
  end
141
106
 
142
107
  def have_unchecked_field(locator)
143
- HaveMatcher.new(:unchecked_field, locator)
108
+ HaveSelector.new(:field, locator, :unchecked => true)
144
109
  end
145
110
 
146
111
  def have_select(locator, options={})
147
- HaveMatcher.new(:select, locator, options)
112
+ HaveSelector.new(:select, locator, options)
148
113
  end
149
114
 
150
115
  def have_table(locator, options={})
151
- HaveMatcher.new(:table, locator, options)
116
+ HaveSelector.new(:table, locator, options)
152
117
  end
153
118
  end
154
119
  end