capybara 0.4.1.2 → 1.0.0.beta1

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 (69) hide show
  1. data/History.txt +46 -0
  2. data/README.rdoc +211 -64
  3. data/lib/capybara.rb +31 -15
  4. data/lib/capybara/cucumber.rb +12 -16
  5. data/lib/capybara/dsl.rb +65 -28
  6. data/lib/capybara/node/actions.rb +7 -5
  7. data/lib/capybara/node/document.rb +8 -0
  8. data/lib/capybara/node/finders.rb +11 -7
  9. data/lib/capybara/node/matchers.rb +32 -6
  10. data/lib/capybara/node/simple.rb +20 -0
  11. data/lib/capybara/rack_test/browser.rb +115 -0
  12. data/lib/capybara/rack_test/driver.rb +77 -0
  13. data/lib/capybara/rack_test/form.rb +80 -0
  14. data/lib/capybara/rack_test/node.rb +101 -0
  15. data/lib/capybara/rspec.rb +11 -3
  16. data/lib/capybara/rspec/features.rb +22 -0
  17. data/lib/capybara/rspec/matchers.rb +146 -0
  18. data/lib/capybara/selector.rb +27 -8
  19. data/lib/capybara/selenium/driver.rb +148 -0
  20. data/lib/capybara/selenium/node.rb +91 -0
  21. data/lib/capybara/session.rb +42 -15
  22. data/lib/capybara/spec/driver.rb +55 -1
  23. data/lib/capybara/spec/fixtures/capybara.jpg +0 -0
  24. data/lib/capybara/spec/public/test.js +7 -2
  25. data/lib/capybara/spec/session.rb +51 -7
  26. data/lib/capybara/spec/session/attach_file_spec.rb +9 -6
  27. data/lib/capybara/spec/session/click_button_spec.rb +35 -0
  28. data/lib/capybara/spec/session/current_host_spec.rb +62 -0
  29. data/lib/capybara/spec/session/fill_in_spec.rb +6 -0
  30. data/lib/capybara/spec/session/find_spec.rb +23 -1
  31. data/lib/capybara/spec/session/first_spec.rb +39 -6
  32. data/lib/capybara/spec/session/has_css_spec.rb +30 -0
  33. data/lib/capybara/spec/session/has_field_spec.rb +47 -11
  34. data/lib/capybara/spec/session/javascript.rb +0 -1
  35. data/lib/capybara/spec/session/text_spec.rb +19 -0
  36. data/lib/capybara/spec/test_app.rb +9 -0
  37. data/lib/capybara/spec/views/form.erb +8 -3
  38. data/lib/capybara/spec/views/header_links.erb +7 -0
  39. data/lib/capybara/spec/views/host_links.erb +12 -0
  40. data/lib/capybara/spec/views/with_html.erb +6 -2
  41. data/lib/capybara/spec/views/with_html_entities.erb +1 -0
  42. data/lib/capybara/spec/views/with_js.erb +4 -0
  43. data/lib/capybara/util/save_and_open_page.rb +7 -3
  44. data/lib/capybara/util/timeout.rb +2 -2
  45. data/lib/capybara/version.rb +1 -1
  46. data/spec/capybara_spec.rb +1 -1
  47. data/spec/driver/rack_test_driver_spec.rb +24 -2
  48. data/spec/driver/selenium_driver_spec.rb +2 -1
  49. data/spec/dsl_spec.rb +56 -4
  50. data/spec/rspec/features_spec.rb +45 -0
  51. data/spec/rspec/matchers_spec.rb +451 -0
  52. data/spec/rspec_spec.rb +9 -2
  53. data/spec/save_and_open_page_spec.rb +9 -13
  54. data/spec/server_spec.rb +4 -0
  55. data/spec/session/rack_test_session_spec.rb +2 -2
  56. data/spec/session/selenium_session_spec.rb +1 -1
  57. data/spec/spec_helper.rb +0 -14
  58. data/spec/string_spec.rb +1 -1
  59. metadata +60 -69
  60. data/lib/capybara/driver/celerity_driver.rb +0 -164
  61. data/lib/capybara/driver/culerity_driver.rb +0 -26
  62. data/lib/capybara/driver/rack_test_driver.rb +0 -303
  63. data/lib/capybara/driver/selenium_driver.rb +0 -161
  64. data/spec/driver/celerity_driver_spec.rb +0 -13
  65. data/spec/driver/culerity_driver_spec.rb +0 -14
  66. data/spec/driver/remote_culerity_driver_spec.rb +0 -22
  67. data/spec/driver/remote_selenium_driver_spec.rb +0 -16
  68. data/spec/session/celerity_session_spec.rb +0 -24
  69. data/spec/session/culerity_session_spec.rb +0 -26
@@ -2,6 +2,13 @@ module Capybara
2
2
  class Selector
3
3
  attr_reader :name
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
  class << self
6
13
  def all
7
14
  @selectors ||= {}
@@ -15,19 +22,26 @@ module Capybara
15
22
  all.delete(name.to_sym)
16
23
  end
17
24
 
18
- def normalize(name_or_locator, locator=nil)
19
- xpath = if locator
20
- all[name_or_locator.to_sym].call(locator)
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]
21
32
  else
22
- selector = all.values.find { |s| s.match?(name_or_locator) }
23
- selector ||= all[Capybara.default_selector]
24
- selector.call(name_or_locator)
33
+ normalized.selector = all.values.find { |s| s.match?(args[0]) }
34
+ normalized.locator = args[0]
25
35
  end
36
+ normalized.selector ||= all[Capybara.default_selector]
37
+
38
+ xpath = normalized.selector.call(normalized.locator)
26
39
  if xpath.respond_to?(:to_xpaths)
27
- xpath.to_xpaths
40
+ normalized.xpaths = xpath.to_xpaths
28
41
  else
29
- [xpath.to_s].flatten
42
+ normalized.xpaths = [xpath.to_s].flatten
30
43
  end
44
+ normalized
31
45
  end
32
46
  end
33
47
 
@@ -46,6 +60,11 @@ module Capybara
46
60
  @match
47
61
  end
48
62
 
63
+ def failure_message(&block)
64
+ @failure_message = block if block
65
+ @failure_message
66
+ end
67
+
49
68
  def call(locator)
50
69
  @xpath.call(locator)
51
70
  end
@@ -0,0 +1,148 @@
1
+ require 'selenium-webdriver'
2
+
3
+ class Capybara::Selenium::Driver < Capybara::Driver::Base
4
+ DEFAULT_OPTIONS = {
5
+ :resynchronize => true,
6
+ :resynchronization_timeout => 10,
7
+ :browser => :firefox
8
+ }
9
+ SPECIAL_OPTIONS = [:browser, :resynchronize, :resynchronization_timeout]
10
+
11
+ attr_reader :app, :rack_server, :options
12
+
13
+ def browser
14
+ unless @browser
15
+ @browser = Selenium::WebDriver.for(options[:browser], options.reject { |key,val| SPECIAL_OPTIONS.include?(key) })
16
+ at_exit do
17
+ @browser.quit
18
+ end
19
+ end
20
+ @browser
21
+ end
22
+
23
+ def initialize(app, options={})
24
+ @app = app
25
+ @options = DEFAULT_OPTIONS.merge(options)
26
+ @rack_server = Capybara::Server.new(@app)
27
+ @rack_server.boot if Capybara.run_server
28
+ end
29
+
30
+ def visit(path)
31
+ browser.navigate.to(url(path))
32
+ end
33
+
34
+ def source
35
+ browser.page_source
36
+ end
37
+
38
+ def body
39
+ browser.page_source
40
+ end
41
+
42
+ def current_url
43
+ browser.current_url
44
+ end
45
+
46
+ def find(selector)
47
+ browser.find_elements(:xpath, selector).map { |node| Capybara::Selenium::Node.new(self, node) }
48
+ end
49
+
50
+ def wait?; true; end
51
+
52
+ def resynchronize
53
+ if options[:resynchronize]
54
+ load_wait_for_ajax_support
55
+ yield
56
+ Capybara.timeout(options[:resynchronization_timeout], self, "failed to resynchronize, ajax request timed out") do
57
+ evaluate_script("!window.capybaraRequestsOutstanding")
58
+ end
59
+ else
60
+ yield
61
+ end
62
+ end
63
+
64
+ def execute_script(script)
65
+ browser.execute_script script
66
+ end
67
+
68
+ def evaluate_script(script)
69
+ browser.execute_script "return #{script}"
70
+ end
71
+
72
+ def reset!
73
+ # Use instance variable directly so we avoid starting the browser just to reset the session
74
+ if @browser
75
+ begin
76
+ @browser.manage.delete_all_cookies
77
+ rescue Selenium::WebDriver::Error::UnhandledError => e
78
+ # delete_all_cookies fails when we've previously gone
79
+ # to about:blank, so we rescue this error and do nothing
80
+ # instead.
81
+ end
82
+ @browser.navigate.to('about:blank')
83
+ end
84
+ end
85
+
86
+ def within_frame(frame_id)
87
+ old_window = browser.window_handle
88
+ browser.switch_to.frame(frame_id)
89
+ yield
90
+ browser.switch_to.window old_window
91
+ end
92
+
93
+ def find_window( selector )
94
+ original_handle = browser.window_handle
95
+ browser.window_handles.each do |handle|
96
+ browser.switch_to.window handle
97
+ if( selector == browser.execute_script("return window.name") ||
98
+ browser.title.include?(selector) ||
99
+ browser.current_url.include?(selector) ||
100
+ (selector == handle) )
101
+ browser.switch_to.window original_handle
102
+ return handle
103
+ end
104
+ end
105
+ raise Capybara::ElementNotFound, "Could not find a window identified by #{selector}"
106
+ end
107
+
108
+ def within_window(selector, &blk)
109
+ handle = find_window( selector )
110
+ browser.switch_to.window(handle, &blk)
111
+ end
112
+
113
+ private
114
+
115
+ def load_wait_for_ajax_support
116
+ browser.execute_script <<-JS
117
+ window.capybaraRequestsOutstanding = 0;
118
+ (function() { // Overriding XMLHttpRequest
119
+ var oldXHR = window.XMLHttpRequest;
120
+
121
+ function newXHR() {
122
+ var realXHR = new oldXHR();
123
+
124
+ window.capybaraRequestsOutstanding++;
125
+ realXHR.addEventListener("readystatechange", function() {
126
+ if( realXHR.readyState == 4 ) {
127
+ setTimeout( function() {
128
+ window.capybaraRequestsOutstanding--;
129
+ if(window.capybaraRequestsOutstanding < 0) {
130
+ window.capybaraRequestsOutstanding = 0;
131
+ }
132
+ }, 500 );
133
+ }
134
+ }, false);
135
+
136
+ return realXHR;
137
+ }
138
+
139
+ window.XMLHttpRequest = newXHR;
140
+ })();
141
+ JS
142
+ end
143
+
144
+ def url(path)
145
+ rack_server.url(path)
146
+ end
147
+
148
+ end
@@ -0,0 +1,91 @@
1
+ class Capybara::Selenium::Node < Capybara::Driver::Node
2
+ def text
3
+ native.text
4
+ end
5
+
6
+ def [](name)
7
+ if name == :value
8
+ value
9
+ else
10
+ native.attribute(name.to_s)
11
+ end
12
+ rescue Selenium::WebDriver::Error::WebDriverError
13
+ nil
14
+ end
15
+
16
+ def value
17
+ if tag_name == "select" and self[:multiple] and not self[:multiple] == "false"
18
+ native.find_elements(:xpath, ".//option").select { |n| n.selected? }.map { |n| n.value || n.text }
19
+ else
20
+ native.value
21
+ end
22
+ end
23
+
24
+ def set(value)
25
+ if tag_name == 'input' and type == 'radio'
26
+ click
27
+ elsif tag_name == 'input' and type == 'checkbox'
28
+ click if value ^ native.attribute('checked').to_s.eql?("true")
29
+ elsif tag_name == 'textarea' or tag_name == 'input'
30
+ resynchronize do
31
+ native.clear
32
+ native.send_keys(value.to_s)
33
+ end
34
+ end
35
+ end
36
+
37
+ def select_option
38
+ resynchronize { native.select }
39
+ end
40
+
41
+ def unselect_option
42
+ if select_node['multiple'] != 'multiple' and select_node['multiple'] != 'true'
43
+ raise Capybara::UnselectNotAllowed, "Cannot unselect option from single select box."
44
+ end
45
+ resynchronize { native.toggle } if selected?
46
+ end
47
+
48
+ def click
49
+ resynchronize { native.click }
50
+ end
51
+
52
+ def drag_to(element)
53
+ resynchronize { native.drag_and_drop_on(element.native) }
54
+ end
55
+
56
+ def tag_name
57
+ native.tag_name
58
+ end
59
+
60
+ def visible?
61
+ displayed = native.displayed?
62
+ displayed and displayed != "false"
63
+ end
64
+
65
+ def selected?
66
+ selected = native.selected?
67
+ selected and selected != "false"
68
+ end
69
+
70
+ alias :checked? :selected?
71
+
72
+ def find(locator)
73
+ native.find_elements(:xpath, locator).map { |n| self.class.new(driver, n) }
74
+ end
75
+
76
+ private
77
+
78
+ def resynchronize
79
+ driver.resynchronize { yield }
80
+ end
81
+
82
+ # a reference to the select node if this is an option node
83
+ def select_node
84
+ find('./ancestor::select').first
85
+ end
86
+
87
+ def type
88
+ self[:type]
89
+ end
90
+
91
+ end
@@ -26,14 +26,24 @@ module Capybara
26
26
  # When using capybara/dsl, the Session is initialized automatically for you.
27
27
  #
28
28
  class Session
29
- DSL_METHODS = [
30
- :all, :first, :attach_file, :body, :check, :choose, :click_link_or_button, :click_button, :click_link, :current_url, :drag, :evaluate_script,
31
- :field_labeled, :fill_in, :find, :find_button, :find_by_id, :find_field, :find_link, :has_content?, :has_css?,
32
- :has_no_content?, :has_no_css?, :has_no_xpath?, :has_xpath?, :locate, :save_and_open_page, :select, :source, :uncheck,
33
- :visit, :wait_until, :within, :within_fieldset, :within_table, :within_frame, :within_window, :has_link?, :has_no_link?, :has_button?,
34
- :has_no_button?, :has_field?, :has_no_field?, :has_checked_field?, :has_unchecked_field?, :has_no_table?, :has_table?,
35
- :unselect, :has_select?, :has_no_select?, :current_path, :click, :has_selector?, :has_no_selector?, :click_on
29
+ NODE_METHODS = [
30
+ :all, :first, :attach_file, :text, :check, :choose,
31
+ :click_link_or_button, :click_button, :click_link, :field_labeled,
32
+ :fill_in, :find, :find_button, :find_by_id, :find_field, :find_link,
33
+ :has_content?, :has_css?, :has_no_content?, :has_no_css?, :has_no_xpath?,
34
+ :has_xpath?, :select, :uncheck, :has_link?, :has_no_link?, :has_button?,
35
+ :has_no_button?, :has_field?, :has_no_field?, :has_checked_field?,
36
+ :has_unchecked_field?, :has_no_table?, :has_table?, :unselect,
37
+ :has_select?, :has_no_select?, :has_selector?, :has_no_selector?,
38
+ :click_on, :has_no_checked_field?, :has_no_unchecked_field?
36
39
  ]
40
+ SESSION_METHODS = [
41
+ :body, :html, :current_url, :current_host, :evaluate_script, :source,
42
+ :visit, :wait_until, :within, :within_fieldset, :within_table,
43
+ :within_frame, :within_window, :current_path, :save_page,
44
+ :save_and_open_page, :reset_session!
45
+ ]
46
+ DSL_METHODS = NODE_METHODS + SESSION_METHODS
37
47
 
38
48
  attr_reader :mode, :app
39
49
 
@@ -60,6 +70,7 @@ module Capybara
60
70
  driver.reset!
61
71
  end
62
72
  alias_method :cleanup!, :reset!
73
+ alias_method :reset_session!, :reset!
63
74
 
64
75
  ##
65
76
  #
@@ -83,11 +94,12 @@ module Capybara
83
94
 
84
95
  ##
85
96
  #
86
- # @return [String] A snapshot of the HTML of the current document, as it looks right now
97
+ # @return [String] A snapshot of the HTML of the current document, as it looks right now (potentially modified by JavaScript).
87
98
  #
88
99
  def body
89
100
  driver.body
90
101
  end
102
+ alias_method :html, :body
91
103
 
92
104
  ##
93
105
  #
@@ -102,7 +114,17 @@ module Capybara
102
114
  # @return [String] Path of the current page, without any domain information
103
115
  #
104
116
  def current_path
105
- URI.parse(current_url).path
117
+ path = URI.parse(current_url).path
118
+ path if path and not path.empty?
119
+ end
120
+
121
+ ##
122
+ #
123
+ # @return [String] Host of the current page
124
+ #
125
+ def current_host
126
+ uri = URI.parse(current_url)
127
+ "#{uri.scheme}://#{uri.host}" if uri.host
106
128
  end
107
129
 
108
130
  ##
@@ -251,6 +273,11 @@ module Capybara
251
273
  #
252
274
  # Save a snapshot of the page and open it in a browser for inspection
253
275
  #
276
+ def save_page
277
+ require 'capybara/util/save_and_open_page'
278
+ Capybara.save_page(body)
279
+ end
280
+
254
281
  def save_and_open_page
255
282
  require 'capybara/util/save_and_open_page'
256
283
  Capybara.save_and_open_page(body)
@@ -260,12 +287,12 @@ module Capybara
260
287
  Capybara::Node::Document.new(self, driver)
261
288
  end
262
289
 
263
- def method_missing(*args)
264
- current_node.send(*args)
265
- end
266
-
267
- def respond_to?(method)
268
- super || current_node.respond_to?(method)
290
+ NODE_METHODS.each do |method|
291
+ class_eval <<-RUBY
292
+ def #{method}(*args, &block)
293
+ current_node.send(:#{method}, *args, &block)
294
+ end
295
+ RUBY
269
296
  end
270
297
 
271
298
  private
@@ -26,6 +26,20 @@ shared_examples_for 'driver' do
26
26
  @driver.visit('/with_simple_html')
27
27
  @driver.body.should include('Bar')
28
28
  end
29
+
30
+ if "".respond_to?(:encoding)
31
+ context "encoding of response between ascii and utf8" do
32
+ it "should be valid with html entities" do
33
+ @driver.visit('/with_html_entities')
34
+ lambda { @driver.body.encode!("UTF-8") }.should_not raise_error
35
+ end
36
+
37
+ it "should be valid without html entities" do
38
+ @driver.visit('/with_html')
39
+ lambda { @driver.body.encode!("UTF-8") }.should_not raise_error
40
+ end
41
+ end
42
+ end
29
43
  end
30
44
 
31
45
  describe '#find' do
@@ -85,6 +99,11 @@ shared_examples_for 'driver' do
85
99
  @driver.find('//option[@value="sv"]')[0].should_not be_selected
86
100
  @driver.find('//h1')[0].should_not be_selected
87
101
  end
102
+
103
+ it "should return document text on /html selector" do
104
+ @driver.visit('/with_simple_html')
105
+ @driver.find('/html')[0].text.should == 'Bar'
106
+ end
88
107
  end
89
108
  end
90
109
  end
@@ -100,7 +119,6 @@ shared_examples_for "driver with javascript support" do
100
119
 
101
120
  describe '#drag_to' do
102
121
  it "should drag and drop an object" do
103
- pending "drag/drop is currently broken under celerity/culerity" if @driver.is_a?(Capybara::Driver::Celerity)
104
122
  draggable = @driver.find('//div[@id="drag"]').first
105
123
  droppable = @driver.find('//div[@id="drop"]').first
106
124
  draggable.drag_to(droppable)
@@ -113,6 +131,42 @@ shared_examples_for "driver with javascript support" do
113
131
  @driver.evaluate_script('1+1').should == 2
114
132
  end
115
133
  end
134
+
135
+ end
136
+
137
+ shared_examples_for "driver with resynchronization support" do
138
+ before { @driver.visit('/with_js') }
139
+ describe "#find" do
140
+ context "with synchronization turned on" do
141
+ it "should wait for all ajax requests to finish" do
142
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
143
+ @driver.find('//p[@id="ajax_request_done"]').should_not be_empty
144
+ end
145
+ end
146
+
147
+ context "with resynchronization turned off" do
148
+ before { @driver.options[:resynchronize] = false }
149
+
150
+ it "should not wait for ajax requests to finish" do
151
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
152
+ @driver.find('//p[@id="ajax_request_done"]').should be_empty
153
+ end
154
+
155
+ after { @driver.options[:resynchronize] = true }
156
+ end
157
+
158
+ context "with short synchronization timeout" do
159
+ before { @driver.options[:resynchronization_timeout] = 0.1 }
160
+
161
+ it "should raise an error" do
162
+ expect do
163
+ @driver.find('//input[@id="fire_ajax_request"]').first.click
164
+ end.to raise_error(Capybara::TimeoutError, "failed to resynchronize, ajax request timed out")
165
+ end
166
+
167
+ after { @driver.options[:resynchronization_timeout] = 10 }
168
+ end
169
+ end
116
170
  end
117
171
 
118
172
  shared_examples_for "driver with header support" do