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
@@ -1,13 +1,7 @@
1
1
  module Capybara
2
2
  class Selector
3
- attr_reader :name
3
+ attr_reader :name, :custom_filters
4
4
 
5
- class Normalized
6
- attr_accessor :selector, :locator, :options, :xpaths
7
-
8
- def failure_message; selector.failure_message; end
9
- def name; selector.name; end
10
- end
11
5
 
12
6
  class << self
13
7
  def all
@@ -21,32 +15,13 @@ module Capybara
21
15
  def remove(name)
22
16
  all.delete(name.to_sym)
23
17
  end
24
-
25
- def normalize(*args)
26
- normalized = Normalized.new
27
- normalized.options = if args.last.is_a?(Hash) then args.pop else {} end
28
-
29
- if args[1]
30
- normalized.selector = all[args[0]]
31
- normalized.locator = args[1]
32
- else
33
- normalized.selector = all.values.find { |s| s.match?(args[0]) }
34
- normalized.locator = args[0]
35
- end
36
- normalized.selector ||= all[Capybara.default_selector]
37
-
38
- xpath = normalized.selector.call(normalized.locator)
39
- if xpath.respond_to?(:to_xpaths)
40
- normalized.xpaths = xpath.to_xpaths
41
- else
42
- normalized.xpaths = [xpath.to_s].flatten
43
- end
44
- normalized
45
- end
46
18
  end
47
19
 
48
20
  def initialize(name, &block)
49
21
  @name = name
22
+ @custom_filters = {}
23
+ @match = nil
24
+ @failure_message = nil
50
25
  instance_eval(&block)
51
26
  end
52
27
 
@@ -55,14 +30,22 @@ module Capybara
55
30
  @xpath
56
31
  end
57
32
 
33
+ # Same as xpath, but wrap in XPath.css().
34
+ def css(&block)
35
+ if block
36
+ @xpath = xpath { |*args| XPath.css(block.call(*args)) }
37
+ end
38
+ @xpath
39
+ end
40
+
58
41
  def match(&block)
59
42
  @match = block if block
60
43
  @match
61
44
  end
62
45
 
63
- def failure_message(&block)
64
- @failure_message = block if block
65
- @failure_message
46
+ def label(label=nil)
47
+ @label = label if label
48
+ @label
66
49
  end
67
50
 
68
51
  def call(locator)
@@ -72,6 +55,10 @@ module Capybara
72
55
  def match?(locator)
73
56
  @match and @match.call(locator)
74
57
  end
58
+
59
+ def filter(name, &block)
60
+ @custom_filters[name] = block
61
+ end
75
62
  end
76
63
  end
77
64
 
@@ -80,10 +67,87 @@ Capybara.add_selector(:xpath) do
80
67
  end
81
68
 
82
69
  Capybara.add_selector(:css) do
83
- xpath { |css| XPath.css(css) }
70
+ css { |css| css }
84
71
  end
85
72
 
86
73
  Capybara.add_selector(:id) do
87
74
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
88
75
  match { |value| value.is_a?(Symbol) }
89
76
  end
77
+
78
+ Capybara.add_selector(:field) do
79
+ xpath { |locator| XPath::HTML.field(locator) }
80
+ filter(:checked) { |node, value| not(value ^ node.checked?) }
81
+ filter(:unchecked) { |node, value| (value ^ node.checked?) }
82
+ filter(:with) { |node, with| node.value == with }
83
+ filter(:type) { |node, type| node[:type] == type }
84
+ end
85
+
86
+ Capybara.add_selector(:fieldset) do
87
+ xpath { |locator| XPath::HTML.fieldset(locator) }
88
+ end
89
+
90
+ Capybara.add_selector(:link_or_button) do
91
+ label "link or button"
92
+ xpath { |locator| XPath::HTML.link_or_button(locator) }
93
+ end
94
+
95
+ Capybara.add_selector(:link) do
96
+ xpath { |locator| XPath::HTML.link(locator) }
97
+ filter(:href) do |node, href|
98
+ node.first(:xpath, XPath.axis(:self)[XPath.attr(:href).equals(href)])
99
+ end
100
+ end
101
+
102
+ Capybara.add_selector(:button) do
103
+ xpath { |locator| XPath::HTML.button(locator) }
104
+ end
105
+
106
+ Capybara.add_selector(:fillable_field) do
107
+ label "field"
108
+ xpath { |locator| XPath::HTML.fillable_field(locator) }
109
+ end
110
+
111
+ Capybara.add_selector(:radio_button) do
112
+ label "radio button"
113
+ xpath { |locator| XPath::HTML.radio_button(locator) }
114
+ filter(:checked) { |node, value| not(value ^ node.checked?) }
115
+ filter(:unchecked) { |node, value| (value ^ node.checked?) }
116
+ end
117
+
118
+ Capybara.add_selector(:checkbox) do
119
+ xpath { |locator| XPath::HTML.checkbox(locator) }
120
+ filter(:checked) { |node, value| not(value ^ node.checked?) }
121
+ filter(:unchecked) { |node, value| (value ^ node.checked?) }
122
+ end
123
+
124
+ Capybara.add_selector(:select) do
125
+ label "select box"
126
+ xpath { |locator| XPath::HTML.select(locator) }
127
+ filter(:options) do |node, options|
128
+ actual = node.all(:xpath, './/option').map { |option| option.text }
129
+ options.sort == actual.sort
130
+ end
131
+ filter(:with_options) { |node, options| options.all? { |option| node.first(:option, option) } }
132
+ filter(:selected) do |node, selected|
133
+ actual = node.all(:xpath, './/option').select { |option| option.selected? }.map { |option| option.text }
134
+ [selected].flatten.sort == actual.sort
135
+ end
136
+ end
137
+
138
+ Capybara.add_selector(:option) do
139
+ xpath { |locator| XPath::HTML.option(locator) }
140
+ end
141
+
142
+ Capybara.add_selector(:file_field) do
143
+ label "file field"
144
+ xpath { |locator| XPath::HTML.file_field(locator) }
145
+ end
146
+
147
+ Capybara.add_selector(:content) do
148
+ xpath { |content| XPath::HTML.content(content) }
149
+ end
150
+
151
+ Capybara.add_selector(:table) do
152
+ xpath { |locator| XPath::HTML.table(locator) }
153
+ end
@@ -2,13 +2,11 @@ require 'selenium-webdriver'
2
2
 
3
3
  class Capybara::Selenium::Driver < Capybara::Driver::Base
4
4
  DEFAULT_OPTIONS = {
5
- :resynchronize => false,
6
- :resynchronization_timeout => 10,
7
5
  :browser => :firefox
8
6
  }
9
- SPECIAL_OPTIONS = [:browser, :resynchronize, :resynchronization_timeout]
7
+ SPECIAL_OPTIONS = [:browser]
10
8
 
11
- attr_reader :app, :rack_server, :options
9
+ attr_reader :app, :options
12
10
 
13
11
  def browser
14
12
  unless @browser
@@ -27,13 +25,13 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
27
25
 
28
26
  def initialize(app, options={})
29
27
  @app = app
28
+ @browser = nil
29
+ @exit_status = nil
30
30
  @options = DEFAULT_OPTIONS.merge(options)
31
- @rack_server = Capybara::Server.new(@app)
32
- @rack_server.boot if Capybara.run_server
33
31
  end
34
32
 
35
33
  def visit(path)
36
- browser.navigate.to(url(path))
34
+ browser.navigate.to(path)
37
35
  end
38
36
 
39
37
  def source
@@ -53,18 +51,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
53
51
  end
54
52
 
55
53
  def wait?; true; end
56
-
57
- def resynchronize
58
- if options[:resynchronize]
59
- load_wait_for_ajax_support
60
- yield
61
- Capybara.timeout(options[:resynchronization_timeout], self, "failed to resynchronize, ajax request timed out") do
62
- evaluate_script("!window.capybaraRequestsOutstanding")
63
- end
64
- else
65
- yield
66
- end
67
- end
54
+ def needs_server?; true; end
68
55
 
69
56
  def execute_script(script)
70
57
  browser.execute_script script
@@ -74,12 +61,15 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
74
61
  browser.execute_script "return #{script}"
75
62
  end
76
63
 
64
+ def save_screenshot(path, options={})
65
+ browser.save_screenshot(path)
66
+ end
67
+
77
68
  def reset!
78
69
  # Use instance variable directly so we avoid starting the browser just to reset the session
79
70
  if @browser
80
- begin
81
- @browser.manage.delete_all_cookies
82
- rescue Selenium::WebDriver::Error::UnhandledError => e
71
+ begin @browser.manage.delete_all_cookies
72
+ rescue Selenium::WebDriver::Error::UnhandledError
83
73
  # delete_all_cookies fails when we've previously gone
84
74
  # to about:blank, so we rescue this error and do nothing
85
75
  # instead.
@@ -92,6 +82,7 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
92
82
  old_window = browser.window_handle
93
83
  browser.switch_to.frame(frame_id)
94
84
  yield
85
+ ensure
95
86
  browser.switch_to.window old_window
96
87
  end
97
88
 
@@ -122,44 +113,6 @@ class Capybara::Selenium::Driver < Capybara::Driver::Base
122
113
  end
123
114
 
124
115
  def invalid_element_errors
125
- [ Selenium::WebDriver::Error::StaleElementReferenceError,
126
- Selenium::WebDriver::Error::InvalidSelectorError,
127
- Selenium::WebDriver::Error::UnknownError ]
128
- end
129
-
130
- private
131
-
132
- def load_wait_for_ajax_support
133
- browser.execute_script <<-JS
134
- window.capybaraRequestsOutstanding = 0;
135
- (function() { // Overriding XMLHttpRequest
136
- var oldXHR = window.XMLHttpRequest;
137
-
138
- function newXHR() {
139
- var realXHR = new oldXHR();
140
-
141
- window.capybaraRequestsOutstanding++;
142
- realXHR.addEventListener("readystatechange", function() {
143
- if( realXHR.readyState == 4 ) {
144
- setTimeout( function() {
145
- window.capybaraRequestsOutstanding--;
146
- if(window.capybaraRequestsOutstanding < 0) {
147
- window.capybaraRequestsOutstanding = 0;
148
- }
149
- }, 500 );
150
- }
151
- }, false);
152
-
153
- return realXHR;
154
- }
155
-
156
- window.XMLHttpRequest = newXHR;
157
- })();
158
- JS
159
- end
160
-
161
- def url(path)
162
- rack_server.url(path)
116
+ [Selenium::WebDriver::Error::ObsoleteElementError, Selenium::WebDriver::Error::UnhandledError]
163
117
  end
164
-
165
118
  end
@@ -23,34 +23,29 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
23
23
  elsif tag_name == 'input' and type == 'checkbox'
24
24
  click if value ^ native.attribute('checked').to_s.eql?("true")
25
25
  elsif tag_name == 'input' and type == 'file'
26
- resynchronize do
27
- native.send_keys(value.to_s)
28
- end
26
+ native.send_keys(value.to_s)
29
27
  elsif tag_name == 'textarea' or tag_name == 'input'
30
- resynchronize do
31
- native.clear
32
- native.send_keys(value.to_s)
33
- end
28
+ native.send_keys(("\b" * native[:value].size) + value.to_s)
34
29
  end
35
30
  end
36
31
 
37
32
  def select_option
38
- resynchronize { native.click } unless selected?
33
+ native.click unless selected?
39
34
  end
40
35
 
41
36
  def unselect_option
42
37
  if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
43
38
  raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
44
39
  end
45
- resynchronize { native.click } if selected?
40
+ native.click if selected?
46
41
  end
47
42
 
48
43
  def click
49
- resynchronize { native.click }
44
+ native.click
50
45
  end
51
46
 
52
47
  def drag_to(element)
53
- resynchronize { driver.browser.action.drag_and_drop(native, element.native).perform }
48
+ driver.browser.action.drag_and_drop(native, element.native).perform
54
49
  end
55
50
 
56
51
  def tag_name
@@ -75,10 +70,6 @@ class Capybara::Selenium::Node < Capybara::Driver::Node
75
70
 
76
71
  private
77
72
 
78
- def resynchronize
79
- driver.resynchronize { yield }
80
- end
81
-
82
73
  # a reference to the select node if this is an option node
83
74
  def select_node
84
75
  find('./ancestor::select').first
@@ -1,11 +1,12 @@
1
1
  require 'uri'
2
2
  require 'net/http'
3
3
  require 'rack'
4
- require 'capybara/util/timeout'
5
4
 
6
5
  module Capybara
7
6
  class Server
8
- class Identify
7
+ class Middleware
8
+ attr_accessor :error
9
+
9
10
  def initialize(app)
10
11
  @app = app
11
12
  end
@@ -14,7 +15,12 @@ module Capybara
14
15
  if env["PATH_INFO"] == "/__identify__"
15
16
  [200, {}, [@app.object_id.to_s]]
16
17
  else
17
- @app.call(env)
18
+ begin
19
+ @app.call(env)
20
+ rescue StandardError => e
21
+ @error = e unless @error
22
+ raise e
23
+ end
18
24
  end
19
25
  end
20
26
  end
@@ -27,23 +33,30 @@ module Capybara
27
33
 
28
34
  attr_reader :app, :port
29
35
 
30
- def initialize(app)
36
+ def initialize(app, port=Capybara.server_port)
31
37
  @app = app
38
+ @middleware = Middleware.new(@app)
39
+ @server_thread = nil # supress warnings
40
+ @port = port
41
+ @port ||= Capybara::Server.ports[@app.object_id]
42
+ @port ||= find_available_port
32
43
  end
33
44
 
34
- def host
35
- "127.0.0.1"
45
+ def reset_error!
46
+ @middleware.error = nil
36
47
  end
37
48
 
38
- def url(path)
39
- if path =~ /^http/
40
- path
41
- else
42
- (Capybara.app_host || "http://#{host}:#{port}") + path.to_s
43
- end
49
+ def error
50
+ @middleware.error
51
+ end
52
+
53
+ def host
54
+ Capybara.server_host || "127.0.0.1"
44
55
  end
45
56
 
46
57
  def responsive?
58
+ return false if @server_thread && @server_thread.join(0)
59
+
47
60
  res = Net::HTTP.start(host, @port) { |http| http.get('/__identify__') }
48
61
 
49
62
  if res.is_a?(Net::HTTPSuccess) or res.is_a?(Net::HTTPRedirection)
@@ -54,25 +67,17 @@ module Capybara
54
67
  end
55
68
 
56
69
  def boot
57
- if @app
58
- @port = Capybara::Server.ports[@app.object_id]
59
-
60
- if not @port or not responsive?
61
- @port = Capybara.server_port || find_available_port
62
- Capybara::Server.ports[@app.object_id] = @port
63
-
64
- Thread.new do
65
- Capybara.server.call(Identify.new(@app), @port)
66
- end
70
+ unless responsive?
71
+ Capybara::Server.ports[@app.object_id] = @port
67
72
 
68
- Capybara.timeout(Capybara.server_boot_timeout) do
69
- if responsive? then true else sleep(0.5) and false end
70
- end
73
+ @server_thread = Thread.new do
74
+ Capybara.server.call(@middleware, @port)
71
75
  end
76
+
77
+ Timeout.timeout(60) { @server_thread.join(0.1) until responsive? }
72
78
  end
73
79
  rescue TimeoutError
74
- puts "Rack application timed out during boot"
75
- exit
80
+ raise "Rack application timed out during boot"
76
81
  else
77
82
  self
78
83
  end