capybara 0.4.1.2 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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