pickles 0.1.0

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