pickles 0.1.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +9 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +10 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +196 -0
  8. data/Rakefile +6 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +8 -0
  11. data/lib/cucumber/pickles.rb +6 -0
  12. data/lib/cucumber/pickles/check_in.rb +10 -0
  13. data/lib/cucumber/pickles/config.rb +61 -0
  14. data/lib/cucumber/pickles/errors/ambigious.rb +27 -0
  15. data/lib/cucumber/pickles/errors/node_find_error.rb +37 -0
  16. data/lib/cucumber/pickles/fill_in.rb +10 -0
  17. data/lib/cucumber/pickles/helpers.rb +40 -0
  18. data/lib/cucumber/pickles/helpers/extensions/chrome/.DS_Store +0 -0
  19. data/lib/cucumber/pickles/helpers/extensions/chrome/compiled.crx.base64 +1 -0
  20. data/lib/cucumber/pickles/helpers/extensions/chrome/manifest.json +15 -0
  21. data/lib/cucumber/pickles/helpers/extensions/chrome/src/.DS_Store +0 -0
  22. data/lib/cucumber/pickles/helpers/extensions/chrome/src/inject/inject.js +35 -0
  23. data/lib/cucumber/pickles/helpers/main.rb +88 -0
  24. data/lib/cucumber/pickles/helpers/node_finders.rb +125 -0
  25. data/lib/cucumber/pickles/helpers/regex.rb +6 -0
  26. data/lib/cucumber/pickles/helpers/waiter.rb +152 -0
  27. data/lib/cucumber/pickles/locator/equal.rb +26 -0
  28. data/lib/cucumber/pickles/locator/index.rb +20 -0
  29. data/lib/cucumber/pickles/refinements.rb +49 -0
  30. data/lib/cucumber/pickles/steps.rb +73 -0
  31. data/lib/cucumber/pickles/steps/can_see.rb +70 -0
  32. data/lib/cucumber/pickles/steps/check.rb +55 -0
  33. data/lib/cucumber/pickles/steps/check_in/complex_input.rb +17 -0
  34. data/lib/cucumber/pickles/steps/check_in/factory.rb +26 -0
  35. data/lib/cucumber/pickles/steps/check_in/input.rb +36 -0
  36. data/lib/cucumber/pickles/steps/check_in/text.rb +17 -0
  37. data/lib/cucumber/pickles/steps/click.rb +91 -0
  38. data/lib/cucumber/pickles/steps/fill.rb +72 -0
  39. data/lib/cucumber/pickles/steps/fill_in/complex_input.rb +19 -0
  40. data/lib/cucumber/pickles/steps/fill_in/factory.rb +25 -0
  41. data/lib/cucumber/pickles/steps/fill_in/input.rb +29 -0
  42. data/lib/cucumber/pickles/steps/fill_in/select.rb +30 -0
  43. data/lib/cucumber/pickles/steps/redirect.rb +3 -0
  44. data/lib/cucumber/pickles/transform.rb +13 -0
  45. data/lib/cucumber/pickles/version.rb +3 -0
  46. data/lib/pickles.rb +3 -0
  47. data/pickles.gemspec +36 -0
  48. data/spec/helpers/node_finders_spec.rb +155 -0
  49. data/spec/helpers/waiter_spec.rb +41 -0
  50. data/spec/locator_spec.rb +31 -0
  51. data/spec/spec_helper.rb +32 -0
  52. data/spec/step_def_spec.rb +0 -0
  53. data/spec/steps/check_in/factory_spec.rb +52 -0
  54. data/spec/steps/fill_in/factory_spec.rb +51 -0
  55. metadata +153 -0
@@ -0,0 +1,10 @@
1
+ module FillIN
2
+
3
+ _dir = 'cucumber/pickles/steps/fill_in/'
4
+
5
+ autoload :Factory, _dir + 'factory'
6
+ autoload :Input, _dir + 'input'
7
+ autoload :Select, _dir + 'select'
8
+ autoload :ComplexInput, _dir + 'complex_input'
9
+
10
+ end
@@ -0,0 +1,40 @@
1
+ unless defined?(SUPPORT_DIR)
2
+ in_features_dir = caller.select { |path| path =~ /features/ }.first
3
+
4
+ if in_features_dir
5
+ features_dir = in_features_dir.split('/')
6
+
7
+ 2.times { features_dir.pop }
8
+
9
+ SUPPORT_DIR = File.join(features_dir,'support')
10
+ end
11
+ end
12
+
13
+ require_relative 'refinements'
14
+ require_relative 'config'
15
+ require_relative 'errors/ambigious'
16
+
17
+ module Locator
18
+
19
+ _dir = 'cucumber/pickles/locator/'
20
+
21
+ autoload :Index, _dir + 'index'
22
+ autoload :Equal, _dir + 'equal'
23
+
24
+ end
25
+
26
+ module Helpers
27
+
28
+ _dir = 'cucumber/pickles/helpers/'
29
+
30
+ autoload :Main, _dir + 'main'
31
+ autoload :Regex, _dir + 'regex'
32
+
33
+ end
34
+
35
+ module Pickles
36
+
37
+ extend Helpers::Main
38
+ include Helpers::Main
39
+
40
+ end
@@ -0,0 +1 @@
1
+ Q3IyNAIAAAAmAQAAAAEAADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALtp8JSC/8xIo1YhrOEu3w08MRVZAtGRzRRig4Xkq+QfRjT52PFlqwuD6Y+tLPaL350aTk4o1ExFb0ycnth/oHkwywjf8V45YA2wo9itWyA4qE77NpYHyO6kpPxe/nOs2580MkVMc/2Cu4xQAoyOZsTmnstZlilow0sNyig4ww8PI0sVIuvtYOdd3r0WNMKRz9gcizYiYAHu+e00Wz5WFZRxKffZyro1M5XfYT1h3ZkIPY+YqFkQME12uk29YE5sNR/nLSd91DuZCOpHmM67Umlcui71omJp29zrPAIAJc+AjVFBiqQt4vnLzoLz5wkSkjgV0/L67aOiqsGrMT0/Ff0CAwEAAU0TBhDaJCyO6hi7PWmA3KFgBRQ0t3tJRypcZwt8V2uIS6dgY6a4O+/L6g8KdxwK5NaIpIpU/fkor68fseCerp82rt2pvSCWyEU9NSXDBJKlkMINaJf8b1EhX7T3yxFwTXQWFldA6UkCMhxYQTCe5KxCp0IRTqfSQ4YZ2Qpy6q6aH7HRLXoulpsrxWQx0W/buDeGicj7ToQdl85gxrqealjdP9Ro8K4yBHwQCqcj0OoM2HQk1HpLAxQRcMe7wVEoPA393PdrNtMpmqrTiiDfjiJsFqSCK8cLWhLn4WtpBrXh95jaaDFrHYsK2bXxVOUYuVAgazjpNMGgnZmnD1MwAylQSwMEFAAACAgAhQMDS9OaFCfEAAAATgEAAA0AAABtYW5pZmVzdC5qc29uVY7RasMwDEXf+xXCzyVr+1jKYOwzRgjG0RqH2G4lORRK/72KnRH2IqF7z5X03AGYaAOaM5jvgVJA6MnPSPA12gfw4IPZL5BK7FNcuENzaI5VDTb6X2TpNvtUjB7Zkb/JGvm/WjTh4xVmSwyS4EbISDPWm4T3rAADYxRgsZK5XnMpimpdXc26+EdlgGep5R1xAxbDXOw0dZkm/jTt/g8Yt1CZmdyHjyM6WVujxGpvKcqxs6LJZeiTy6F8IZakwi+t7e71BlBLAwQUAAAICAAQoQJLAAAAAAIAAAAAAAAABAAAAHNyYy8DAFBLAwQUAAAICABQAwNLAAAAAAIAAAAAAAAACwAAAHNyYy9pbmplY3QvAwBQSwMEFAAACAgAUAMDS5pHPma7AQAAdwUAABQAAABzcmMvaW5qZWN0L2luamVjdC5qc52TMW/bMBCFd/8KRkMk2RTdAJkiaAoCZLBRoO7QIQsjXSKiEqmSZ7uu4P/eo2zIduE6crQR773T3XdkXlpTg4DfCNopo4UDXczBOfkOUbvl7G2pcyQhsuAaox3ErB2NGFtJy6BClrHC5MsaNIrcgkR4qsCfosDlVjUYxKm3k1UorcE+f5/PKBT4vKmKrw3o7Md89ozYfINfS3AoGmvQ4KYBYUhN10oXZi0ktbGCvcdlX/jFVNb3Lfkrz3nBIW7PVppMOJbKCVkUTytqfKYcsQAbvQQ0T7FxSEPlpdTv8BLwvmzc3mdZF+xcC++6vT37hyTZ8pu7mO/HFbmsqshH+aG3bRoQJwLV0yyprJANBYrHUlVFRAh3LP3n8TncVPD/BYSdHlJmF+mOwhOiTIi08mnuXHgqH+8oHLM2ZJO9Hk7Hj4sFQyvpongEbjw9UllikoOW0DYasLh5YNpoYDeqboxFqTE9ydTmz2dS7hOhNbz+VHh9kF2bOOL0Zmz9D6aek9eG0vnYyw5UBphPYAwpPtS4m11qVcszV+Tw497x8VTEYLjbXFPZDTdfMG7D/lGyS8+3e1/+AY+2cfoXUEsBAgAAFAAACAgAhQMDS9OaFCfEAAAATgEAAA0AAAAAAAAAAQAAAAAAAAAAAG1hbmlmZXN0Lmpzb25QSwECAAAUAAAICAAQoQJLAAAAAAIAAAAAAAAABAAAAAAAAAAAAAAAAADvAAAAc3JjL1BLAQIAABQAAAgIAFADA0sAAAAAAgAAAAAAAAALAAAAAAAAAAAAAAAAABMBAABzcmMvaW5qZWN0L1BLAQIAABQAAAgIAFADA0uaRz5muwEAAHcFAAAUAAAAAAAAAAEAAAAAAD4BAABzcmMvaW5qZWN0L2luamVjdC5qc1BLBQYAAAAABAAEAOgAAAArAwAAAAA=
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Chrome driver Ajax shim",
3
+ "version": "0.0.1",
4
+ "manifest_version": 2,
5
+ "description": "Chrome driver testing vars to preserve Ajax requests sent status",
6
+ "content_scripts": [
7
+ {
8
+ "matches": ["<all_urls>"],
9
+ "js": [
10
+ "src/inject/inject.js"
11
+ ],
12
+ "run_at": "document_start"
13
+ }
14
+ ]
15
+ }
@@ -0,0 +1,35 @@
1
+ chrome.extension.sendMessage({}, function(response) {
2
+
3
+ var elt = document.createElement("script");
4
+
5
+ elt.innerHTML = "var oldOpen=XMLHttpRequest.prototype.open;window.activeRequests=0,XMLHttpRequest.prototype.open=function(a,b,c,d,e){window.activeRequests++,this.addEventListener(\"readystatechange\",function(){4==this.readyState&&window.activeRequests--},!1),oldOpen.call(this,a,b,c,d,e)};";
6
+
7
+
8
+ document.head.appendChild(elt);
9
+
10
+ var style = document.createElement('style');
11
+ style.type = 'text/css';
12
+ style.innerHTML = '* {' +
13
+ '/*CSS transitions*/' +
14
+ ' -o-transition-property: none !important;' +
15
+ ' -moz-transition-property: none !important;' +
16
+ ' -ms-transition-property: none !important;' +
17
+ ' -webkit-transition-property: none !important;' +
18
+ ' transition-property: none !important;' +
19
+ // '/*CSS transforms*/' +
20
+ // ' -o-transform: none !important;' +
21
+ // ' -moz-transform: none !important;' +
22
+ // ' -ms-transform: none !important;' +
23
+ // ' -webkit-transform: none !important;' +
24
+ // ' transform: none !important;' +
25
+ ' /*CSS animations*/' +
26
+ ' -webkit-animation: none !important;' +
27
+ ' -moz-animation: none !important;' +
28
+ ' -o-animation: none !important;' +
29
+ ' -ms-animation: none !important;' +
30
+ ' animation: none !important;}';
31
+
32
+ document.head.appendChild(style);
33
+
34
+
35
+ });
@@ -0,0 +1,88 @@
1
+ require_relative 'node_finders'
2
+ require_relative 'waiter'
3
+
4
+ module Helpers::Main
5
+
6
+ include NodeFinders
7
+ include Waiter
8
+
9
+ #
10
+ # parent node of given
11
+ #
12
+ # @node - Capybara node
13
+ #
14
+ # returns Capybara node
15
+ #
16
+ def parent_node(node)
17
+ node.find(:xpath, '..', wait: 0, visible: false)
18
+ end
19
+
20
+ #
21
+ # trigger blur event on given node
22
+ #
23
+ # @node - Capybara node
24
+ #
25
+ def blur(node)
26
+ trigger(node, 'blur')
27
+
28
+ Capybara.current_session.execute_script("document.body.click()")
29
+ end
30
+
31
+ #
32
+ # Select checkbox | radio input
33
+ #
34
+ # @input - Capybara node with <input type="checkbox|radio">
35
+ # @value - optional - value to set to input, Defaults to input state switch
36
+ #
37
+ # returns: [void]
38
+ #
39
+ def select_input(input, value = nil)
40
+ case value
41
+ when "true", true
42
+ value = true
43
+ when "false", false
44
+ value = false
45
+ else
46
+ value = !input.checked?
47
+ end
48
+
49
+ #
50
+ # Hack:
51
+ # cant use input.set(#{value})
52
+ # because element can be hidden or covered by other eement
53
+ # in which case Selenium raises error
54
+ #
55
+ trigger(parent_node(input), 'click')
56
+
57
+ Capybara.current_session.execute_script("arguments[0].checked = #{value}", input)
58
+ end
59
+
60
+ #
61
+ # Attach file from features/support/attachments/* to given file input
62
+ #
63
+ # @input - Capybara node with <input type="file">
64
+ # @file - file path relative to features/support/attachments/*
65
+ #
66
+ # returns [void]
67
+ #
68
+ def attach_file(input, file)
69
+ path = File.expand_path(File.join(SUPPORT_DIR,"attachments/#{file}"))
70
+
71
+ raise RuntimeError, "file '#{path}' does not exists" unless File.exists?(path)
72
+
73
+ input.set(path)
74
+ end
75
+
76
+ #
77
+ # Triggers event on node
78
+ # Usefull when Capybara raises error about element being covered by another
79
+ #
80
+ # @node - Capybara node
81
+ # @event - event to trigger
82
+ #
83
+ # returns: [void]
84
+ def trigger(node, event)
85
+ Capybara.current_session.execute_script("arguments[0].#{event}()", node)
86
+ end
87
+
88
+ end
@@ -0,0 +1,125 @@
1
+ module NodeFinders
2
+
3
+ using BlankMethod
4
+
5
+ #
6
+ # Finds text node by text locator
7
+ #
8
+ # @text - locator ( see locator docs in #Artifact )
9
+ # @within: - within block to limit search to
10
+ #
11
+ # returns Capybara node
12
+ #
13
+ def find_node(locator, within: nil)
14
+ within ||= Capybara.current_session
15
+
16
+ locator, index = Locator::Index.execute(locator)
17
+ locator, xpath = Locator::Equal.execute(locator)
18
+
19
+ if index
20
+ xpath = "(#{xpath})[#{index}]"
21
+ end
22
+
23
+ _rescued_find([:xpath, xpath, wait: 0, visible: false], locator, within: within, message: "find_node") do
24
+ raise Capybara::ElementNotFound,
25
+ "Unable to find node by locator #{locator}",
26
+ caller
27
+ end
28
+ end
29
+
30
+ #
31
+ # Does lookup based on provided in config maps
32
+ #
33
+ def detect_node(el_alias, locator = nil, within: nil)
34
+ return find_node(locator, within: within) if el_alias.blank?
35
+
36
+ within ||= Capybara.current_session
37
+
38
+ locator, index = Locator::Index.execute(locator)
39
+
40
+ if index.nil?
41
+ el_alias, index = Locator::Index.execute(el_alias.to_s)
42
+ end
43
+
44
+ el_alias = el_alias.to_sym
45
+
46
+ if xpath = Pickles.config.xpath_node_map[el_alias]
47
+ xpath = xpath.respond_to?(:call) ? xpath.call(locator) : xpath
48
+
49
+ search_params = [:xpath, xpath, wait: 0]
50
+ elsif css = Pickles.config.css_node_map[el_alias] || el_alias
51
+ css = css.respond_to?(:call) ? css.call(locator) : css
52
+
53
+ search_params = [:css, css, text: locator, wait: 0]
54
+ end
55
+
56
+ if index
57
+ within.all(*search_params)[index - 1]
58
+ else
59
+ _rescued_find(search_params, locator || el_alias, within: within, message: "Detecting by #{xpath || css}") do
60
+ raise Capybara::ElementNotFound,
61
+ "Unable to detect node by locator #{locator}",
62
+ caller
63
+ end
64
+ end
65
+ end
66
+
67
+ #
68
+ # Similar to find_node, but looking for fillable fields for this cases:
69
+ # 1. label or span(as label) with input hint for input || textarea || @contenteditable
70
+ # 2. capybara lookup by label || plcaeholder || id || name || etc
71
+ # 3. @contenteditable with @placeholder = @locator
72
+ #
73
+ # @input_locator - string to identify input field by
74
+ #
75
+ # returns: Capybara node
76
+ #
77
+ def find_input(input_locator, within: nil, options: {})
78
+ within ||= Capybara.current_session
79
+ options[:wait] = 0
80
+ options[:visible] = false
81
+
82
+ locator, index = Locator::Index.execute(input_locator)
83
+
84
+ if index
85
+ index_xpath = "[#{index}]"
86
+ end
87
+
88
+ xpath = ".//*[@contenteditable and (@placeholder='#{locator}' or name='#{locator}')]#{index_xpath}"
89
+
90
+ # case 3
91
+ _rescued_find([:xpath, xpath, options], locator, within: within, message: "@contenteditable with placeholder = #{locator}") do
92
+
93
+ locator, label_xpath = Locator::Equal.execute(input_locator)
94
+
95
+ inputtable_field_xpath = "*[self::input or self::textarea or @contenteditable]"
96
+
97
+ xpath = "(#{label_xpath})#{index_xpath}/ancestor::*[.//#{inputtable_field_xpath}][position()=1]//#{inputtable_field_xpath}"
98
+
99
+ # case 1
100
+ _rescued_find([:xpath, xpath, options], locator, within: within, message: "find_node(#{locator}) => look for closest fillable field") do
101
+
102
+ # case 2
103
+ _rescued_find([:fillable_field, locator, options], locator, within: within, message: 'Capybara#fillable_input') do
104
+
105
+ # all cases failed => raise
106
+ raise Capybara::ElementNotFound,
107
+ "Unable to find fillable field by locator #{locator}",
108
+ caller
109
+
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+ end
116
+
117
+ def _rescued_find(params, locator, within:, message:)
118
+ within.find(*params)
119
+ rescue Capybara::Ambiguous => err # Capybara::Ambiguous < Capybara::ElementNotFound == true
120
+ raise Pickles::Ambiguous.new(locator, within, params, message), nil, caller
121
+ rescue Capybara::ElementNotFound
122
+ yield
123
+ end
124
+
125
+ end
@@ -0,0 +1,6 @@
1
+ # :nodoc:
2
+ module Helpers::Regex
3
+
4
+ WITHIN = /\A\s*(.*)?\s*(?:["|'](.*?)["|'])?\s*\Z/
5
+
6
+ end
@@ -0,0 +1,152 @@
1
+ # original code:
2
+ #
3
+ # (function() {
4
+ # var oldOpen = XMLHttpRequest.prototype.open;
5
+ # window.openHTTPs = 0;
6
+ # XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
7
+ # window.openHTTPs++;
8
+ # this.addEventListener("readystatechange", function() {
9
+ # if(this.readyState == 4) {
10
+ # window.openHTTPs--;
11
+ # }
12
+ # }, false);
13
+ # oldOpen.call(this, method, url, async, user, pass);
14
+ # }
15
+ # })(XMLHttpRequest);
16
+ #
17
+ # module Capybara
18
+ # module Selenium
19
+ # class Driver
20
+ #
21
+ # class << self
22
+ # alias __pickles_redefined__new new
23
+ #
24
+ # #
25
+ # # Monkey patch initializer to load custom chrome extension in extensions/chrome
26
+ # #
27
+ # # It will add window.activeRequests to keep track of active AJAX requests in tests
28
+ # #
29
+ # # For source code of extension see extensions/chrome/src/inject/inject.js
30
+ # #
31
+ # # TODO: support all major browser drivers
32
+ # #
33
+ # def new(app, options={})
34
+ # if options[:browser].to_s == "chrome"
35
+ # options[:desired_capabilities] ||= {}
36
+ # options[:desired_capabilities]["chromeOptions"] ||= {}
37
+ # options[:desired_capabilities]["chromeOptions"]["extensions"] ||= []
38
+ #
39
+ # extension_path = File.expand_path('extensions/chrome/compiled.crx.base64', __dir__)
40
+ #
41
+ # options[:desired_capabilities]["chromeOptions"]["extensions"].unshift(File.read(extension_path))
42
+ # end
43
+ #
44
+ # __pickles_redefined__new(app, options)
45
+ # end
46
+ # end
47
+ #
48
+ # end
49
+ #
50
+ # end
51
+ # end
52
+
53
+ def stub_xml_http_request(page)
54
+ page.evaluate_script <<-JAVASCRIPT
55
+ (function() {
56
+
57
+ if (window.ajaxRequestIsSet) { return; };
58
+ window.ajaxRequestIsSet = true;
59
+
60
+ var oldOpen = XMLHttpRequest.prototype.open;
61
+ window.activeRequests = 0;
62
+ XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
63
+ window.activeRequests++;
64
+ this.addEventListener("readystatechange", function() {
65
+ if (this.readyState == 4) {
66
+ window.activeRequests--;
67
+
68
+ #{
69
+ if Pickles.config.log_xhr_response
70
+ <<-LOG
71
+ if (parseInt(this.status, 10) >= 400) {
72
+ console.error("############## ERRRO RESPONSE START ################");
73
+ console.error(this.response);
74
+ console.error("############## ERRRO RESPONSE END ################");
75
+ }
76
+ LOG
77
+ end
78
+ }
79
+
80
+ }
81
+ }, false);
82
+ oldOpen.call(this, method, url, async, user, pass);
83
+ };
84
+
85
+
86
+ var style = document.createElement('style');
87
+ style.type = 'text/css';
88
+ style.innerHTML = '* {' +
89
+ '/*CSS transitions*/' +
90
+ ' -o-transition-property: none !important;' +
91
+ ' -moz-transition-property: none !important;' +
92
+ ' -ms-transition-property: none !important;' +
93
+ ' -webkit-transition-property: none !important;' +
94
+ ' transition-property: none !important;' +
95
+ ' /*CSS animations*/' +
96
+ ' -webkit-animation: none !important;' +
97
+ ' -moz-animation: none !important;' +
98
+ ' -o-animation: none !important;' +
99
+ ' -ms-animation: none !important;' +
100
+ ' animation: none !important;}';
101
+ document.getElementsByTagName('head')[0].appendChild(style);
102
+
103
+ })();
104
+ JAVASCRIPT
105
+ end
106
+
107
+ module Capybara
108
+ class Session
109
+
110
+ alias __pickles_redefined__old_visit visit
111
+
112
+ def visit(*args)
113
+ __pickles_redefined__old_visit(*args)
114
+
115
+ stub_xml_http_request(Capybara.current_session)
116
+ end
117
+
118
+ end
119
+ end
120
+
121
+ module Waiter
122
+
123
+ module_function
124
+
125
+ def wait
126
+ wait_for_ajax
127
+
128
+ return unless block_given?
129
+
130
+ page.document.synchronize do
131
+ yield
132
+ end
133
+ end
134
+
135
+ def page
136
+ Capybara.current_session
137
+ end
138
+
139
+ #
140
+ # waits for all Ajax requests to finish
141
+ #
142
+ def wait_for_ajax
143
+ page.document.synchronize do
144
+ pending_ajax_requests_num.zero? || raise(Capybara::ElementNotFound)
145
+ end
146
+ end
147
+
148
+ def pending_ajax_requests_num
149
+ page.evaluate_script("window.activeRequests")
150
+ end
151
+
152
+ end