ruby_raider 3.13.0 → 4.0.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 (225) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/runtime.yml +38 -0
  3. data/.github/workflows/system_tests.yml +1 -23
  4. data/.reek.yml +1 -0
  5. data/.rspec +1 -0
  6. data/Gemfile +2 -0
  7. data/README.md +16 -49
  8. data/RELEASE.md +22 -0
  9. data/bin/raider +5 -0
  10. data/lib/commands/scaffolding_commands.rb +2 -25
  11. data/lib/commands/utility_commands.rb +23 -3
  12. data/lib/generators/automation/automation_generator.rb +0 -5
  13. data/lib/generators/automation/templates/component.tt +1 -0
  14. data/lib/generators/automation/templates/home.tt +2 -2
  15. data/lib/generators/automation/templates/login.tt +6 -26
  16. data/lib/generators/automation/templates/page.tt +23 -18
  17. data/lib/generators/automation/templates/partials/home_page_selector.tt +1 -2
  18. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -8
  19. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +1 -2
  20. data/lib/generators/automation/templates/partials/url_methods.tt +8 -2
  21. data/lib/generators/automation/templates/partials/visit_method.tt +0 -5
  22. data/lib/generators/automation/templates/pdp.tt +1 -9
  23. data/lib/generators/common_generator.rb +0 -2
  24. data/lib/generators/generator.rb +12 -19
  25. data/lib/generators/helper_generator.rb +21 -19
  26. data/lib/generators/infrastructure/templates/azure_pipelines.tt +1 -1
  27. data/lib/generators/infrastructure/templates/github.tt +2 -3
  28. data/lib/generators/infrastructure/templates/github_appium.tt +0 -8
  29. data/lib/generators/invoke_generators.rb +4 -23
  30. data/lib/generators/menu_generator.rb +1 -9
  31. data/lib/generators/rspec/templates/accessibility_spec.tt +1 -9
  32. data/lib/generators/rspec/templates/spec.tt +8 -24
  33. data/lib/generators/template_renderer/partial_cache.rb +3 -1
  34. data/lib/generators/template_renderer.rb +1 -1
  35. data/lib/generators/templates/common/gemfile.tt +3 -10
  36. data/lib/generators/templates/common/git_ignore.tt +0 -5
  37. data/lib/generators/templates/common/partials/mobile_config.tt +3 -1
  38. data/lib/generators/templates/common/partials/web_config.tt +10 -1
  39. data/lib/generators/templates/common/rakefile.tt +0 -15
  40. data/lib/generators/templates/common/read_me.tt +357 -250
  41. data/lib/generators/templates/common/rubocop.tt +0 -2
  42. data/lib/generators/templates/helpers/driver_helper.tt +8 -0
  43. data/lib/generators/templates/helpers/gesture_helper.tt +78 -0
  44. data/lib/generators/templates/helpers/network_helper.tt +521 -0
  45. data/lib/generators/templates/helpers/partials/quit_driver.tt +0 -2
  46. data/lib/generators/templates/helpers/partials/screenshot.tt +0 -2
  47. data/lib/generators/templates/helpers/partials/selenium_driver.tt +5 -2
  48. data/lib/generators/templates/helpers/screenshot_helper.tt +490 -0
  49. data/lib/generators/templates/helpers/spec_helper.tt +39 -29
  50. data/lib/generators/templates/helpers/trace_helper.tt +240 -95
  51. data/lib/generators/templates/helpers/video_helper.tt +3 -8
  52. data/lib/ruby_raider/runtime/actionability.rb +129 -0
  53. data/lib/ruby_raider/runtime/actionable.rb +49 -0
  54. data/lib/ruby_raider/runtime/by_role.rb +55 -0
  55. data/lib/ruby_raider/runtime/config.rb +79 -0
  56. data/lib/ruby_raider/runtime/context.rb +38 -0
  57. data/lib/ruby_raider/runtime/errors.rb +69 -0
  58. data/lib/ruby_raider/runtime/finders.rb +73 -0
  59. data/lib/ruby_raider/runtime/js/actionability.js +134 -0
  60. data/lib/ruby_raider/runtime/js/role_engine.js +315 -0
  61. data/lib/ruby_raider/runtime/locator_converter.rb +72 -0
  62. data/lib/ruby_raider/runtime/matchers/base.rb +222 -0
  63. data/lib/ruby_raider/runtime/matchers/element.rb +183 -0
  64. data/lib/ruby_raider/runtime/matchers/page.rb +71 -0
  65. data/lib/ruby_raider/runtime/matchers/presence.rb +41 -0
  66. data/lib/ruby_raider/runtime/matchers/retrying_block.rb +104 -0
  67. data/lib/ruby_raider/runtime/observer.rb +170 -0
  68. data/lib/ruby_raider/runtime/role_finder.rb +83 -0
  69. data/lib/ruby_raider/runtime/rspec.rb +78 -0
  70. data/lib/ruby_raider/runtime/selectors.rb +60 -0
  71. data/lib/ruby_raider/runtime/strict.rb +142 -0
  72. data/lib/ruby_raider/runtime/wait.rb +84 -0
  73. data/lib/ruby_raider/runtime.rb +20 -0
  74. data/lib/scaffolding/name_normalizer.rb +1 -1
  75. data/lib/scaffolding/project_detector.rb +2 -28
  76. data/lib/scaffolding/scaffolding.rb +0 -20
  77. data/lib/scaffolding/templates/component.tt +0 -14
  78. data/lib/scaffolding/templates/page_object.tt +2 -14
  79. data/lib/scaffolding/templates/spec.tt +1 -9
  80. data/lib/utilities/utilities.rb +10 -8
  81. data/lib/version +1 -1
  82. data/ruby_raider.gemspec +2 -1
  83. data/sig/commands/scaffolding_commands.rbs +0 -2
  84. data/sig/commands/utility_commands.rbs +6 -0
  85. data/sig/generators/generator.rbs +0 -4
  86. data/sig/generators/helper_generator.rbs +0 -1
  87. data/sig/generators/menu_generator.rbs +0 -1
  88. data/sig/ruby_raider/runtime/actionability.rbs +25 -0
  89. data/sig/ruby_raider/runtime/actionable.rbs +13 -0
  90. data/sig/ruby_raider/runtime/by_role.rbs +15 -0
  91. data/sig/ruby_raider/runtime/config.rbs +26 -0
  92. data/sig/ruby_raider/runtime/context.rbs +12 -0
  93. data/sig/ruby_raider/runtime/errors.rbs +36 -0
  94. data/sig/ruby_raider/runtime/finders.rbs +26 -0
  95. data/sig/ruby_raider/runtime/locator_converter.rbs +21 -0
  96. data/sig/ruby_raider/runtime/matchers/base.rbs +64 -0
  97. data/sig/ruby_raider/runtime/matchers/element.rbs +78 -0
  98. data/sig/ruby_raider/runtime/matchers/page.rbs +38 -0
  99. data/sig/ruby_raider/runtime/matchers/presence.rbs +21 -0
  100. data/sig/ruby_raider/runtime/matchers/retrying_block.rbs +27 -0
  101. data/sig/ruby_raider/runtime/observer.rbs +46 -0
  102. data/sig/ruby_raider/runtime/role_finder.rbs +18 -0
  103. data/sig/ruby_raider/runtime/selectors.rbs +21 -0
  104. data/sig/ruby_raider/runtime/strict.rbs +35 -0
  105. data/sig/ruby_raider/runtime/wait.rbs +17 -0
  106. data/sig/ruby_raider/runtime.rbs +2 -0
  107. data/sig/scaffolding/project_detector.rbs +0 -4
  108. data/sig/scaffolding/scaffolding.rbs +0 -5
  109. data/sig/utilities/utilities.rbs +2 -1
  110. data/sig/vendor/selenium_webdriver.rbs +71 -0
  111. data/spec/commands/raider_commands_spec.rb +38 -3
  112. data/spec/commands/scaffolding_commands_spec.rb +27 -0
  113. data/spec/commands/utility_commands_spec.rb +53 -0
  114. data/spec/generators/gesture_helper_spec.rb +123 -0
  115. data/spec/generators/network_helper_spec.rb +634 -0
  116. data/spec/generators/screenshot_helper_spec.rb +776 -0
  117. data/spec/generators/template_renderer_spec.rb +0 -4
  118. data/spec/generators/trace_helper_spec.rb +489 -97
  119. data/spec/generators/video_helper_spec.rb +0 -9
  120. data/spec/integration/commands/browser_update_after_creation_spec.rb +0 -8
  121. data/spec/integration/commands/scaffolding_commands_spec.rb +12 -70
  122. data/spec/integration/commands/utility_commands_spec.rb +19 -41
  123. data/spec/integration/content/azure_content_spec.rb +2 -6
  124. data/spec/integration/content/ci_content_spec.rb +1 -55
  125. data/spec/integration/content/common_content_spec.rb +1 -118
  126. data/spec/integration/content/config_content_spec.rb +34 -31
  127. data/spec/integration/content/gemfile_content_spec.rb +7 -72
  128. data/spec/integration/content/helper_content_spec.rb +105 -99
  129. data/spec/integration/content/page_content_spec.rb +85 -87
  130. data/spec/integration/content/readme_content_spec.rb +134 -75
  131. data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
  132. data/spec/integration/content/rubocop_content_spec.rb +1 -1
  133. data/spec/integration/content/syntax_validation_spec.rb +2 -2
  134. data/spec/integration/content/test_content_spec.rb +23 -147
  135. data/spec/integration/end_to_end_features_spec.rb +1 -43
  136. data/spec/integration/end_to_end_spec.rb +319 -326
  137. data/spec/integration/generators/automation_generator_spec.rb +3 -37
  138. data/spec/integration/generators/axe_addon_spec.rb +2 -35
  139. data/spec/integration/generators/azure_generator_spec.rb +1 -1
  140. data/spec/integration/generators/common_generator_spec.rb +1 -40
  141. data/spec/integration/generators/config_features_spec.rb +1 -24
  142. data/spec/integration/generators/github_generator_spec.rb +0 -12
  143. data/spec/integration/generators/helpers_generator_spec.rb +74 -80
  144. data/spec/integration/generators/invoke_generators_spec.rb +54 -45
  145. data/spec/integration/generators/rspec_generator_spec.rb +0 -6
  146. data/spec/integration/scaffolding_e2e_spec.rb +17 -179
  147. data/spec/integration/spec_helper.rb +4 -6
  148. data/spec/integration/trace_format_checker.rb +11 -2
  149. data/spec/integration/trace_page_server.rb +72 -5
  150. data/spec/menus/menu_generator_spec.rb +14 -39
  151. data/spec/runtime/actionability_spec.rb +207 -0
  152. data/spec/runtime/actionable_spec.rb +125 -0
  153. data/spec/runtime/browser/actionability_script_browser_spec.rb +150 -0
  154. data/spec/runtime/browser/actionable_browser_spec.rb +98 -0
  155. data/spec/runtime/browser/finders_browser_spec.rb +148 -0
  156. data/spec/runtime/browser/matchers_browser_spec.rb +67 -0
  157. data/spec/runtime/browser/retrying_block_browser_spec.rb +37 -0
  158. data/spec/runtime/browser/role_engine_browser_spec.rb +184 -0
  159. data/spec/runtime/browser/strict_browser_spec.rb +38 -0
  160. data/spec/runtime/by_role_spec.rb +52 -0
  161. data/spec/runtime/config_spec.rb +79 -0
  162. data/spec/runtime/context_spec.rb +73 -0
  163. data/spec/runtime/errors_spec.rb +89 -0
  164. data/spec/runtime/finders_spec.rb +165 -0
  165. data/spec/runtime/fixtures/actionability.html +70 -0
  166. data/spec/runtime/fixtures/locators.html +46 -0
  167. data/spec/runtime/fixtures/matchers.html +33 -0
  168. data/spec/runtime/fixtures/roles.html +162 -0
  169. data/spec/runtime/matchers/base_spec.rb +165 -0
  170. data/spec/runtime/matchers/element_spec.rb +277 -0
  171. data/spec/runtime/matchers/page_spec.rb +102 -0
  172. data/spec/runtime/matchers/presence_spec.rb +59 -0
  173. data/spec/runtime/matchers/retrying_block_spec.rb +165 -0
  174. data/spec/runtime/observer_spec.rb +341 -0
  175. data/spec/runtime/role_finder_spec.rb +156 -0
  176. data/spec/runtime/role_query_spec.rb +75 -0
  177. data/spec/runtime/selectors_spec.rb +135 -0
  178. data/spec/runtime/strict_spec.rb +257 -0
  179. data/spec/runtime/support/browser.rb +48 -0
  180. data/spec/runtime/support/fake_bridge.rb +117 -0
  181. data/spec/runtime/wait_spec.rb +71 -0
  182. data/spec/scaffolding/name_normalizer_spec.rb +3 -2
  183. data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -43
  184. data/spec/system/selenium_spec.rb +1 -5
  185. data/spec/system/support/system_test_helper.rb +2 -5
  186. data/spec/utilities/headless_config_spec.rb +0 -7
  187. data/spec/utilities/utilities_spec.rb +24 -0
  188. metadata +113 -48
  189. data/lib/generators/automation/templates/header.tt +0 -9
  190. data/lib/generators/cucumber/cucumber_generator.rb +0 -58
  191. data/lib/generators/cucumber/templates/accessibility_feature.tt +0 -5
  192. data/lib/generators/cucumber/templates/accessibility_steps.tt +0 -17
  193. data/lib/generators/cucumber/templates/cucumber.tt +0 -1
  194. data/lib/generators/cucumber/templates/env.tt +0 -7
  195. data/lib/generators/cucumber/templates/feature.tt +0 -21
  196. data/lib/generators/cucumber/templates/partials/appium_env.tt +0 -26
  197. data/lib/generators/cucumber/templates/partials/driver_world.tt +0 -18
  198. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +0 -20
  199. data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -120
  200. data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -48
  201. data/lib/generators/cucumber/templates/partials/watir_world.tt +0 -4
  202. data/lib/generators/cucumber/templates/partials/web_steps.tt +0 -28
  203. data/lib/generators/cucumber/templates/steps.tt +0 -5
  204. data/lib/generators/cucumber/templates/user_factory.tt +0 -15
  205. data/lib/generators/cucumber/templates/world.tt +0 -7
  206. data/lib/generators/templates/helpers/browser_helper.tt +0 -31
  207. data/lib/generators/templates/helpers/raider_element.tt +0 -159
  208. data/lib/generators/templates/helpers/wait_helper.tt +0 -611
  209. data/lib/scaffolding/templates/feature.tt +0 -6
  210. data/lib/scaffolding/templates/steps.tt +0 -19
  211. data/sig/generators/cucumber/cucumber_generator.rbs +0 -12
  212. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +0 -175
  213. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +0 -24
  214. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +0 -37
  215. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +0 -33
  216. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +0 -174
  217. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +0 -37
  218. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +0 -33
  219. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +0 -49
  220. data/spec/integration/content/cucumber_reporter_content_spec.rb +0 -161
  221. data/spec/integration/generators/cucumber_generator_spec.rb +0 -100
  222. data/spec/integration/helpers/wait_helper_mobile_spec.rb +0 -281
  223. data/spec/integration/helpers/wait_helper_spec.rb +0 -679
  224. data/spec/integration/helpers/wait_helper_support.rb +0 -183
  225. data/spec/system/watir_spec.rb +0 -38
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime'
5
+ require_relative 'support/fake_bridge'
6
+
7
+ RSpec.describe 'Raider::Actionability' do
8
+ let(:bridge) { Raider::Spec::FakeBridge.new }
9
+ let(:element) { Selenium::WebDriver::Element.new(bridge, 'e1') }
10
+ let(:overlay) { { 'check' => 'events', 'covering' => { 'element' => '<div class="overlay">', 'x' => 412, 'y' => 300 } } }
11
+
12
+ before do
13
+ Raider.reset
14
+ Raider::Actionability.reset_notice
15
+ allow($stderr).to receive(:puts)
16
+ end
17
+
18
+ after { Raider.reset }
19
+
20
+ it 'makes no script call when actionability is off' do
21
+ Raider.actionability = false
22
+ Raider::Actionability.ensure(element, :click, force: false)
23
+ expect(bridge.script_calls).to be_empty
24
+ end
25
+
26
+ it 'makes no script call but one tag_name command when forced' do
27
+ bridge.answer(:element_tag_name, 'div')
28
+ Raider::Actionability.ensure(element, :click, force: true)
29
+ expect([bridge.script_calls, bridge.calls]).to eq([[], [[:element_tag_name, 'e1']]])
30
+ end
31
+
32
+ it 'polls until the check returns nil' do
33
+ bridge.script_results.push({ 'check' => 'visible' }, { 'check' => 'visible' }, nil)
34
+ Raider::Actionability.ensure(element, :click, force: false)
35
+ expect(bridge.script_calls.size).to eq(3)
36
+ end
37
+
38
+ it 'pauses briefly and re-checks at once after a first stable poll' do
39
+ first = { 'check' => 'stable', 'first' => true }
40
+ bridge.script_results.push(first, nil)
41
+ allow(Raider::Actionability).to receive(:sleep)
42
+ Raider::Actionability.ensure(element, :click, force: false)
43
+ expect(Raider::Actionability).to have_received(:sleep).with(0.02).once
44
+ expect(bridge.script_calls.size).to eq(2)
45
+ end
46
+
47
+ it 'passes the script and the action checks to the browser' do
48
+ bridge.script_results.push(nil)
49
+ Raider::Actionability.ensure(element, :submit, force: false)
50
+ expect(bridge.script_calls.first).to eq([Raider::Actionability::SCRIPT, element, %w[visible enabled inert]])
51
+ end
52
+
53
+ it 'raises NotActionable naming the covering element' do
54
+ Raider.timeout = 0.3
55
+ bridge.script_results.concat([overlay] * 20)
56
+ expect { Raider::Actionability.ensure(element, :click, force: false) }
57
+ .to raise_error(Raider::NotActionable, /receive events; `<div class="overlay">` covers it at \(412, 300\)/)
58
+ end
59
+
60
+ it 'returns zero when actionability is off or forced' do
61
+ Raider.actionability = false
62
+ off = Raider::Actionability.ensure(element, :click, force: false)
63
+ Raider.actionability = true
64
+ bridge.answer(:element_tag_name, 'div')
65
+ expect([off, Raider::Actionability.ensure(element, :click, force: true)]).to eq([0, 0])
66
+ end
67
+
68
+ it 'returns the number of not-ready polls on the web' do
69
+ bridge.script_results.push({ 'check' => 'visible' }, { 'check' => 'enabled' }, nil)
70
+ expect(Raider::Actionability.ensure(element, :click, force: false)).to eq(2)
71
+ end
72
+
73
+ it 'does not count the first stable poll as not ready' do
74
+ bridge.script_results.push({ 'check' => 'stable', 'first' => true }, nil)
75
+ allow(Raider::Actionability).to receive(:sleep)
76
+ expect(Raider::Actionability.ensure(element, :click, force: false)).to eq(0)
77
+ end
78
+
79
+ describe 'on an Appium native context' do
80
+ let(:context) { 'NATIVE_APP' }
81
+ let(:appium) do
82
+ name = context
83
+ Class.new(Raider::Spec::FakeBridge) { define_method(:current_context) { name } }.new
84
+ end
85
+
86
+ def native = @native ||= Selenium::WebDriver::Element.new(appium, 'e1')
87
+
88
+ def ensure_native(action, **opts)
89
+ Raider::Actionability.ensure(native, action, force: false, **opts)
90
+ end
91
+
92
+ it 'polls until displayed and enabled, then returns the not-ready count without a script call' do
93
+ appium.answer(:element_displayed?, false, false, true).answer(:element_enabled?, false, true)
94
+ expect(ensure_native(:click)).to eq(3)
95
+ expect(appium.script_calls).to be_empty
96
+ end
97
+
98
+ it 'returns zero when ready at once' do
99
+ appium.answer(:element_displayed?, true).answer(:element_enabled?, true)
100
+ expect(ensure_native(:click)).to eq(0)
101
+ end
102
+
103
+ %i[click send_keys clear].each do |action|
104
+ it "raises NotActionable to be visible for a hidden element on #{action}" do
105
+ Raider.timeout = 0.3
106
+ appium.answer(:element_displayed?, false).answer(:element_enabled?, true)
107
+ expect { ensure_native(action) }.to raise_error(Raider::NotActionable, /waited 0\.[34]\d?s for .* to be visible/)
108
+ end
109
+
110
+ it "raises NotActionable to be enabled for a disabled element on #{action}" do
111
+ Raider.timeout = 0.3
112
+ appium.answer(:element_displayed?, true).answer(:element_enabled?, false)
113
+ expect { ensure_native(action) }.to raise_error(Raider::NotActionable, /waited 0\.[34]\d?s for .* to be enabled/)
114
+ end
115
+ end
116
+
117
+ it 'makes no check call for submit' do
118
+ allow(native).to receive(:displayed?)
119
+ allow(native).to receive(:enabled?)
120
+ expect(ensure_native(:submit)).to eq(0)
121
+ expect(native).not_to have_received(:displayed?)
122
+ expect(native).not_to have_received(:enabled?)
123
+ expect([appium.script_calls, appium.calls]).to eq([[], []])
124
+ end
125
+
126
+ it 'raises a stale element at once rather than waiting out the timeout' do
127
+ Raider.timeout = 5
128
+ allow(native).to receive(:displayed?).and_raise(Selenium::WebDriver::Error::StaleElementReferenceError)
129
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
+ expect { ensure_native(:click) }.to raise_error(Selenium::WebDriver::Error::StaleElementReferenceError)
131
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 1
132
+ expect(native).to have_received(:displayed?).once
133
+ end
134
+
135
+ it 'reports the elapsed wait of the gate, not the configured timeout' do
136
+ Raider.timeout = 5
137
+ appium.answer(:element_displayed?, false)
138
+ error = nil
139
+ Raider::Wait.until(timeout: 0.2) do
140
+ ensure_native(:click)
141
+ rescue Raider::NotActionable => e
142
+ error = e
143
+ end
144
+ expect(error.message).to match(/waited 0\.[23]\d?s for/)
145
+ end
146
+
147
+ it 'raises KeyError for an action with no native table entry' do
148
+ expect { ensure_native(:hover) }.to raise_error(KeyError)
149
+ end
150
+
151
+ it 'makes one displayed call and no enabled call when forced' do
152
+ allow(native).to receive(:displayed?).and_return(true)
153
+ allow(native).to receive(:enabled?)
154
+ expect(Raider::Actionability.ensure(native, :click, force: true)).to eq(0)
155
+ expect(native).to have_received(:displayed?).once
156
+ expect(native).not_to have_received(:enabled?)
157
+ end
158
+
159
+ it 'raises on a stale element when forced' do
160
+ appium.answer(:element_displayed?, Selenium::WebDriver::Error::StaleElementReferenceError)
161
+ expect { Raider::Actionability.ensure(native, :click, force: true) }
162
+ .to raise_error(Selenium::WebDriver::Error::StaleElementReferenceError)
163
+ end
164
+
165
+ it 'makes no displayed or enabled call when actionability is off' do
166
+ Raider.actionability = false
167
+ allow(native).to receive(:displayed?)
168
+ allow(native).to receive(:enabled?)
169
+ expect(ensure_native(:click)).to eq(0)
170
+ expect(native).not_to have_received(:displayed?)
171
+ expect(native).not_to have_received(:enabled?)
172
+ end
173
+
174
+ it 'asks for the context once per action' do
175
+ appium.answer(:element_displayed?, true).answer(:element_enabled?, true)
176
+ allow(appium).to receive(:current_context).and_call_original
177
+ ensure_native(:click)
178
+ expect(appium).to have_received(:current_context).once
179
+ end
180
+
181
+ context 'when the context is a webview' do
182
+ let(:context) { 'WEBVIEW_1' }
183
+
184
+ it 'uses the script path as before' do
185
+ appium.script_results.push({ 'check' => 'visible' }, nil)
186
+ expect(ensure_native(:click)).to eq(1)
187
+ expect(appium.script_calls.size).to eq(2)
188
+ end
189
+ end
190
+ end
191
+
192
+ it 'prints the notice once' do
193
+ bridge.script_results.push(nil, nil)
194
+ 2.times { Raider::Actionability.ensure(element, :click, force: false) }
195
+ expect($stderr).to have_received(:puts).with(/actionability on/).once
196
+ end
197
+
198
+ it 'prints no notice with RAIDER_QUIET=1' do
199
+ bridge.script_results.push(nil)
200
+ old = ENV.fetch('RAIDER_QUIET', nil)
201
+ ENV['RAIDER_QUIET'] = '1'
202
+ Raider::Actionability.ensure(element, :click, force: false)
203
+ expect($stderr).not_to have_received(:puts)
204
+ ensure
205
+ old ? ENV['RAIDER_QUIET'] = old : ENV.delete('RAIDER_QUIET')
206
+ end
207
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../lib/ruby_raider/runtime'
5
+ require_relative 'support/fake_bridge'
6
+
7
+ RSpec.describe 'Raider::Actionable' do
8
+ let(:bridge) { Raider::Spec::FakeBridge.new }
9
+ let(:element) { Selenium::WebDriver::Element.new(bridge, 'e1') }
10
+ let(:keys) { Selenium::WebDriver::Keys.encode(['a', :enter]) }
11
+
12
+ before do
13
+ Raider.reset
14
+ Raider::Actionability.reset_notice
15
+ allow($stderr).to receive(:puts)
16
+ end
17
+
18
+ after { Raider.reset }
19
+
20
+ it 'is prepended to Selenium::WebDriver::Element' do
21
+ expect(Selenium::WebDriver::Element.ancestors.first).to eq(Raider::Actionable)
22
+ end
23
+
24
+ it 'defines only the four action methods and the send_key alias' do
25
+ expect(Raider::Actionable.instance_methods(false)).to contain_exactly(:click, :send_keys, :send_key, :clear, :submit)
26
+ end
27
+
28
+ it 'polls the script until it passes, then clicks' do
29
+ bridge.script_results.push({ 'check' => 'visible' }, nil)
30
+ element.click
31
+ expect([bridge.script_calls.size, bridge.calls.last]).to eq([2, [:click_element, 'e1']])
32
+ end
33
+
34
+ it 'clicks after one tag_name command when forced' do
35
+ bridge.answer(:element_tag_name, 'div')
36
+ element.click(force: true)
37
+ expect([bridge.script_calls, bridge.calls]).to eq([[], [[:element_tag_name, 'e1'], [:click_element, 'e1']]])
38
+ end
39
+
40
+ it 'sends the keys Selenium would send' do
41
+ bridge.script_results.push(nil)
42
+ element.send_keys('a', :enter)
43
+ expect(bridge.calls.last).to eq([:send_keys_to_element, 'e1', keys])
44
+ end
45
+
46
+ it 'gates the send_key alias too' do
47
+ bridge.script_results.push(nil)
48
+ element.send_key('a')
49
+ expect([bridge.script_calls.size, bridge.calls.last]).to eq([1, [:send_keys_to_element, 'e1', Selenium::WebDriver::Keys.encode(['a'])]])
50
+ end
51
+
52
+ it 'sends keys when forced without leaking the keyword' do
53
+ bridge.answer(:element_tag_name, 'div')
54
+ element.send_keys('a', :enter, force: true)
55
+ expect(bridge.calls.last).to eq([:send_keys_to_element, 'e1', keys])
56
+ end
57
+
58
+ it 'clears after the check' do
59
+ bridge.script_results.push(nil)
60
+ element.clear
61
+ expect([bridge.script_calls.size, bridge.calls.last]).to eq([1, [:clear_element, 'e1']])
62
+ end
63
+
64
+ it 'submits after the check' do
65
+ bridge.script_results.push(nil)
66
+ element.submit
67
+ expect([bridge.script_calls.size, bridge.calls.last]).to eq([1, [:submit_element, 'e1']])
68
+ end
69
+
70
+ it 'makes no script call when actionability is off' do
71
+ Raider.actionability = false
72
+ element.click
73
+ expect([bridge.script_calls, bridge.calls]).to eq([[], [[:click_element, 'e1']]])
74
+ end
75
+
76
+ it 'raises a stale element at once without acting' do
77
+ stale = Class.new(Raider::Spec::FakeBridge) do
78
+ def execute_script(*) = raise(Selenium::WebDriver::Error::StaleElementReferenceError, 'stale')
79
+ end.new
80
+ expect { Selenium::WebDriver::Element.new(stale, 'e1').click }
81
+ .to raise_error(Selenium::WebDriver::Error::StaleElementReferenceError)
82
+ expect(stale.calls).to be_empty
83
+ end
84
+
85
+ it 'lets a script timeout from the page propagate' do
86
+ hung = Class.new(Raider::Spec::FakeBridge) do
87
+ def execute_script(*) = raise(Selenium::WebDriver::Error::ScriptTimeoutError, 'hung')
88
+ end.new
89
+ expect { Selenium::WebDriver::Element.new(hung, 'e1').click }
90
+ .to raise_error(Selenium::WebDriver::Error::ScriptTimeoutError)
91
+ end
92
+
93
+ it 'gates subclasses such as the Appium element' do
94
+ appium_like = Class.new(Selenium::WebDriver::Element)
95
+ stub_const('AppiumLikeElement', appium_like)
96
+ bridge.script_results.push(nil)
97
+ appium_like.new(bridge, 'e1').click
98
+ expect([bridge.script_calls.size, bridge.calls.last]).to eq([1, [:click_element, 'e1']])
99
+ end
100
+
101
+ describe 'on an Appium native context' do
102
+ let(:appium) { Class.new(Raider::Spec::FakeBridge) { def current_context = 'NATIVE_APP' }.new }
103
+ let(:native) { Selenium::WebDriver::Element.new(appium, 'e1') }
104
+
105
+ before { appium.answer(:element_displayed?, false, true).answer(:element_enabled?, false, true) }
106
+
107
+ it 'polls displayed and enabled, then clicks without a script call' do
108
+ native.click
109
+ expect(appium.calls).to include([:click_element, 'e1'])
110
+ expect(appium.script_calls).to be_empty
111
+ end
112
+
113
+ it 'clicks after one displayed call when forced' do
114
+ allow(native).to receive(:displayed?).and_return(false)
115
+ native.click(force: true)
116
+ expect(native).to have_received(:displayed?).once
117
+ expect(appium.calls).to include([:click_element, 'e1'])
118
+ end
119
+
120
+ it 'sends keys once ready' do
121
+ native.send_keys('a')
122
+ expect(appium.calls).to include([:send_keys_to_element, 'e1', ['a']])
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../support/browser'
6
+
7
+ RSpec.describe 'Raider actionability script in a real browser', :browser do
8
+ let(:driver) { Raider::Spec::Browser.driver }
9
+ let(:click) { Raider::Actionability::CHECKS[:click] }
10
+ let(:send_keys) { Raider::Actionability::CHECKS[:send_keys] }
11
+
12
+ before { Raider::Spec::Browser.open('actionability.html') }
13
+
14
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
15
+
16
+ def element(id)
17
+ driver.find_element(id: id)
18
+ end
19
+
20
+ def run(id, checks)
21
+ driver.execute_script(Raider::Actionability::SCRIPT, element(id), checks)
22
+ end
23
+
24
+ # Stable needs a previous poll to compare with, so poll twice like the gate does.
25
+ def settled(id, checks)
26
+ run(id, checks)
27
+ run(id, checks)
28
+ end
29
+
30
+ it 'passes a plain button once it has been polled twice' do
31
+ expect(run('plain', click)).to eq('check' => 'stable', 'first' => true)
32
+ expect(run('plain', click)).to be_nil
33
+ end
34
+
35
+ it 'fails visible for display: none and visibility: hidden' do
36
+ expect(run('hidden-display', click)).to eq('check' => 'visible')
37
+ expect(run('hidden-visibility', click)).to eq('check' => 'visible')
38
+ end
39
+
40
+ it 'scrolls an off-screen button into view and passes' do
41
+ expect(settled('far', click)).to be_nil
42
+ end
43
+
44
+ it 'reports the overlay covering a button with integer coordinates' do
45
+ result = settled('covered', click)
46
+ expect(result['check']).to eq('events')
47
+ expect(result['covering']['element']).to eq('div#overlay')
48
+ expect(result['covering']['x']).to be_a(Integer)
49
+ expect(result['covering']['y']).to be_a(Integer)
50
+ end
51
+
52
+ it 'reports the sticky header covering a button after the script scrolls' do
53
+ result = settled('under-header', click)
54
+ expect(result['check']).to eq('events')
55
+ expect(result['covering']['element']).to eq('header#top')
56
+ end
57
+
58
+ it 'fails enabled for a disabled button and a button in a disabled fieldset' do
59
+ expect(run('disabled', click)).to eq('check' => 'enabled')
60
+ expect(run('in-fieldset', click)).to eq('check' => 'enabled')
61
+ end
62
+
63
+ it 'treats the first legend of a disabled fieldset as enabled' do
64
+ expect(settled('legend-button', click)).to be_nil
65
+ end
66
+
67
+ it 'fails editable for a readonly input and passes a writable one and contenteditable' do
68
+ expect(run('readonly', send_keys)).to eq('check' => 'editable')
69
+ expect(run('writable', send_keys)).to be_nil
70
+ expect(run('area', send_keys)).to be_nil
71
+ expect(run('editable', send_keys)).to be_nil
72
+ end
73
+
74
+ it 'passes editable for a button and the body, which keys such as Enter and Escape target' do
75
+ expect(run('plain', send_keys)).to be_nil
76
+ expect(driver.execute_script(Raider::Actionability::SCRIPT, driver.find_element(tag_name: 'body'), send_keys)).to be_nil
77
+ end
78
+
79
+ it 'fails inert inside an [inert] region' do
80
+ expect(run('in-inert', click)).to eq('check' => 'inert')
81
+ end
82
+
83
+ it 'fails inert outside an open modal dialog and passes inside it' do
84
+ driver.execute_script("document.getElementById('dlg').showModal()")
85
+ expect(run('outside-dialog', click)).to eq('check' => 'inert')
86
+ expect(settled('in-dialog', click)).to be_nil
87
+ end
88
+
89
+ it 'reports a moving button as not stable on consecutive polls' do
90
+ expect(run('moving', click)).to eq('check' => 'stable', 'first' => true)
91
+ sleep 0.1
92
+ expect(run('moving', click)).to eq('check' => 'stable')
93
+ sleep 0.1
94
+ expect(run('moving', click)).to eq('check' => 'stable')
95
+ end
96
+
97
+ it 'reports an element moving vertically in a scrollable page as not stable' do
98
+ expect(run('bouncer', click)).to eq('check' => 'stable', 'first' => true)
99
+ sleep 0.1
100
+ expect(run('bouncer', click)).to eq('check' => 'stable')
101
+ sleep 0.1
102
+ expect(run('bouncer', click)).to eq('check' => 'stable')
103
+ end
104
+
105
+ it 'passes a static button on the second poll' do
106
+ run('plain', click)
107
+ sleep 0.1
108
+ expect(run('plain', click)).to be_nil
109
+ end
110
+
111
+ it 'passes editable for a select' do
112
+ expect(run('choices', send_keys)).to be_nil
113
+ end
114
+
115
+ it 'fails visible for a file input inside a display: none container, except when typing a path' do
116
+ expect(run('file-hidden', %w[visible])).to eq('check' => 'visible')
117
+ expect(run('file-hidden', click)).to eq('check' => 'visible')
118
+ expect(run('file-hidden', send_keys)).to be_nil
119
+ end
120
+
121
+ it 'fails visible for a zero-sized button' do
122
+ expect(run('zero', click)).to eq('check' => 'visible')
123
+ end
124
+
125
+ it 'scrolls a button clipped inside an overflow container into view' do
126
+ expect(settled('deep', click)).to be_nil
127
+ end
128
+
129
+ it 'passes inert for a button in a shadow root inside the open modal dialog' do
130
+ driver.execute_script("document.getElementById('dlg').showModal()")
131
+ button = driver.execute_script("return document.getElementById('shadow-host').shadowRoot.getElementById('shadow-button')")
132
+ driver.execute_script(Raider::Actionability::SCRIPT, button, click)
133
+ expect(driver.execute_script(Raider::Actionability::SCRIPT, button, click)).to be_nil
134
+ end
135
+
136
+ it 'skips stable and events for an option' do
137
+ expect(run('opt-a', click)).to be_nil
138
+ end
139
+
140
+ it 'passes visible for a file input with an empty rect' do
141
+ expect(run('file', %w[visible])).to be_nil
142
+ end
143
+
144
+ it 'keeps the previous rect in a WeakMap and not on the element' do
145
+ run('plain', click)
146
+ expect(driver.execute_script('return typeof window.__raiderRects')).to eq('object')
147
+ expect(driver.execute_script("return '__raiderRect' in document.getElementById('plain')")).to be(false)
148
+ expect(driver.execute_script('return window.__raider === undefined')).to be(true)
149
+ end
150
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../support/browser'
6
+
7
+ RSpec.describe 'Raider::Actionable in a real browser', :browser do
8
+ let(:driver) { Raider::Spec::Browser.driver }
9
+
10
+ before do
11
+ Raider.reset
12
+ Raider.timeout = 1
13
+ Raider::Spec::Browser.open('actionability.html')
14
+ end
15
+
16
+ after { Raider.reset }
17
+
18
+ after(:all) { Raider::Spec::Browser.quit } # rubocop:disable RSpec/BeforeAfterAll
19
+
20
+ def element(id)
21
+ driver.find_element(id: id)
22
+ end
23
+
24
+ it 'refuses to click a button the overlay covers' do
25
+ expect { element('covered').click }
26
+ .to raise_error(Raider::NotActionable, /receive events; `div#overlay` covers it/)
27
+ end
28
+
29
+ it 'skips the checks with force: true and leaves the covered click to Selenium' do
30
+ expect { element('covered').click(force: true) }
31
+ .to raise_error(Selenium::WebDriver::Error::ElementClickInterceptedError)
32
+ end
33
+
34
+ it 'scrolls to and clicks an off-screen button' do
35
+ element('far').click
36
+ expect(element('far').attribute('data-clicked')).to eq('1')
37
+ end
38
+
39
+ it 'refuses a button in a disabled fieldset' do
40
+ expect { element('in-fieldset').click }.to raise_error(Raider::NotActionable, /to be enabled/)
41
+ end
42
+
43
+ it 'refuses a button inside a closed details' do
44
+ expect { element('in-details').click }.to raise_error(Raider::NotActionable, /to be visible/)
45
+ end
46
+
47
+ it 'names the sticky header covering a button' do
48
+ expect { element('under-header').click }.to raise_error(Raider::NotActionable, /`header#top` covers it/)
49
+ end
50
+
51
+ it 'refuses to type into a readonly input' do
52
+ expect { element('readonly').send_keys('x') }.to raise_error(Raider::NotActionable, /to be editable/)
53
+ end
54
+
55
+ it 'types into a contenteditable element' do
56
+ element('editable').send_keys('hello')
57
+ expect(element('editable').text).to include('hello')
58
+ end
59
+
60
+ it 'clicks a button clipped inside an overflow container' do
61
+ element('deep').click
62
+ expect(element('deep').attribute('data-clicked')).to eq('1')
63
+ end
64
+
65
+ it 'sends keys to the body' do
66
+ expect { driver.find_element(tag_name: 'body').send_keys(:escape) }.not_to raise_error
67
+ end
68
+
69
+ it 'refuses a moving button' do
70
+ expect { element('moving').click }.to raise_error(Raider::NotActionable, /to be stable/)
71
+ end
72
+
73
+ it 'lets Selenium select an option by text' do
74
+ Selenium::WebDriver::Support::Select.new(element('choices')).select_by(:text, 'Blue')
75
+ expect(element('choices').attribute('value')).to eq('blue')
76
+ end
77
+
78
+ it 'leaves the covered click to Selenium when actionability is off' do
79
+ Raider.actionability = false
80
+ expect { element('covered').click }.to raise_error(Selenium::WebDriver::Error::ElementClickInterceptedError)
81
+ end
82
+
83
+ it 'costs under 50 ms per first click on distinct visible buttons', :slow do
84
+ mean_ms = lambda do
85
+ Raider::Spec::Browser.open('actionability.html')
86
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
87
+ 10.times { |index| element("m#{index}").click }
88
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) / 10 * 1000
89
+ end
90
+ Raider.actionability = true
91
+ on_mean = mean_ms.call
92
+ Raider.actionability = false
93
+ off_mean = mean_ms.call
94
+ puts format('actionability on %<on>.1f ms, off %<off>.1f ms, difference %<diff>.1f ms per first click',
95
+ on: on_mean, off: off_mean, diff: on_mean - off_mean)
96
+ expect(on_mean - off_mean).to be < 50
97
+ end
98
+ end