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
@@ -1,679 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'wait_helper_support'
4
-
5
- WaitHelperSpec.load_templates(mobile: false)
6
-
7
- module WaitHelperSpec
8
- class Page
9
- include WaitHelper
10
-
11
- attr_reader :driver
12
-
13
- def initialize(driver)
14
- @driver = driver
15
- end
16
- end
17
-
18
- # A <select> answering Support::Select's option queries.
19
- class FakeSelect < FakeElement
20
- OPTIONS = [%w[Red red], %w[Blue blue], %w[Green green]].freeze
21
-
22
- def initialize(**state)
23
- super(tag_name: 'select', **state)
24
- @options = OPTIONS.each_with_index.map do |(label, value), index|
25
- FakeElement.new(text: label, attributes: { 'value' => value }, index:)
26
- end
27
- end
28
-
29
- attr_reader :options
30
-
31
- def find_elements(xpath: nil, tag_name: nil, **locator)
32
- return super(**locator) unless xpath || tag_name
33
- return @options if tag_name
34
-
35
- text = xpath[/normalize-space\(\.\) = "(.*)"\]/, 1]
36
- value = xpath[/@value = "(.*)"\]/, 1]
37
- @options.select { |option| option.text == text || option.attribute('value') == value }
38
- end
39
- end
40
- end
41
-
42
- describe 'WaitHelper' do
43
- let(:driver) { WaitHelperSpec::FakeDriver.new }
44
- let(:page) { WaitHelperSpec::Page.new(driver) }
45
-
46
- def element(**state) = WaitHelperSpec::FakeElement.new(**state)
47
- def clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
48
- def errors = Selenium::WebDriver::Error
49
-
50
- describe '#find_element' do
51
- it 'returns the element once it is present and visible' do
52
- found = element
53
- driver.script({ id: 'x' }, nil, element(displayed: false), found)
54
- expect(page.find_element({ id: 'x' }, timeout: 1)).to eq(found)
55
- expect(driver.finds.size).to eq(3)
56
- end
57
-
58
- it 'returns a hidden element with visible: false' do
59
- hidden = element(displayed: false)
60
- driver.script({ id: 'x' }, hidden)
61
- expect(page.find_element({ id: 'x' }, visible: false, timeout: 1)).to eq(hidden)
62
- end
63
-
64
- it 'symbolizes a string-keyed locator' do
65
- driver.script({ id: 'x' }, element)
66
- page.find_element({ 'id' => 'x' }, timeout: 1)
67
- expect(driver.finds).to eq([{ id: 'x' }])
68
- end
69
-
70
- it 'raises ArgumentError at once for an empty or a two-key locator' do
71
- started = clock
72
- expect { page.find_element({}) }.to raise_error(ArgumentError)
73
- expect { page.find_element({ id: 'a', css: 'b' }) }.to raise_error(ArgumentError)
74
- expect(clock - started).to be < 0.1
75
- end
76
-
77
- it 'makes one attempt at timeout: 0' do
78
- driver.script({ id: 'hidden' }, element(displayed: false))
79
- expect { page.find_element({ id: 'hidden' }, timeout: 0) }
80
- .to raise_error(WaitHelper::TimeoutError, /last seen: present but not visible/)
81
- expect(driver.finds.size).to eq(1)
82
- end
83
- end
84
-
85
- describe '#find_elements' do
86
- it 'returns every match, hidden ones included, once one is present' do
87
- shown = element
88
- hidden = element(displayed: false)
89
- driver.script_many({ css: 'li' }, [], [shown, hidden])
90
- expect(page.find_elements({ css: 'li' }, timeout: 1)).to eq([shown, hidden])
91
- end
92
-
93
- it 'returns [] at the timeout without raising' do
94
- expect(page.find_elements({ css: 'li' }, timeout: 0.2)).to eq([])
95
- end
96
- end
97
-
98
- describe '#click' do
99
- it 'waits through not found, hidden, disabled and moving, then clicks once' do
100
- target = element(rects: [[0, 0, 10, 10], [5, 5, 10, 10], [5, 5, 10, 10]])
101
- driver.script({ id: 'go' }, nil, element(displayed: false), element(enabled: false), target)
102
- page.click({ id: 'go' }, timeout: 2)
103
- expect(target.log).to eq([[:click]])
104
- expect(driver.finds.size).to eq(6)
105
- end
106
-
107
- it 'retries intercepted, stale and not-interactable clicks, re-finding by locator' do
108
- target = element(errors: [errors::ElementClickInterceptedError.new('covered'),
109
- errors::StaleElementReferenceError.new('stale'),
110
- errors::ElementNotInteractableError.new('not interactable')])
111
- driver.script({ id: 'go' }, target)
112
- page.click({ id: 'go' }, timeout: 3)
113
- expect(target.log).to eq([[:click]])
114
- expect(driver.finds.size).to be > 3
115
- end
116
-
117
- it 'raises InvalidSelectorError on the first attempt' do
118
- driver.script({ css: '[[' }, errors::InvalidSelectorError)
119
- expect { page.click({ css: '[[' }, timeout: 1) }.to raise_error(errors::InvalidSelectorError)
120
- expect(driver.finds.size).to eq(1)
121
- end
122
-
123
- it 'resolves a per-OS locator with appium_device' do
124
- android = WaitHelperSpec::FakeDriver.new(device: :android)
125
- android.script({ accessibility_id: 'b' }, element)
126
- per_os = { ios: { accessibility_id: 'a' }, android: { accessibility_id: 'b' } }
127
- WaitHelperSpec::Page.new(android).click(per_os, timeout: 1)
128
- expect(android.finds.uniq).to eq([{ accessibility_id: 'b' }])
129
- end
130
-
131
- it 'raises a stale passed element at once, suggesting a locator' do
132
- passed = element
133
- passed.go_stale
134
- expect { page.click(passed, timeout: 2) }.to raise_error(errors::StaleElementReferenceError, /pass .*locator/)
135
- end
136
- end
137
-
138
- describe 'typing and forms' do
139
- it 'passes every key to send_keys' do
140
- field = element
141
- driver.script({ id: 'x' }, field)
142
- page.send_keys({ id: 'x' }, 'a', :tab, timeout: 1)
143
- expect(field.log).to eq([[:send_keys, 'a', :tab]])
144
- end
145
-
146
- it 'clears and submits once visible and enabled, returning the element' do
147
- field = element
148
- driver.script({ id: 'x' }, element(enabled: false), field)
149
- expect(page.clear({ id: 'x' }, timeout: 1)).to eq(field)
150
- expect(page.submit({ id: 'x' }, timeout: 1)).to eq(field)
151
- expect(field.log).to eq([[:clear], [:submit]])
152
- end
153
-
154
- it 'selects by text, value and index through Support::Select' do
155
- dropdown = WaitHelperSpec::FakeSelect.new
156
- driver.script({ id: 'colour' }, dropdown)
157
- page.select_by({ id: 'colour' }, :text, 'Blue', timeout: 1)
158
- page.select_by({ id: 'colour' }, :value, 'green', timeout: 1)
159
- page.select_by({ id: 'colour' }, :index, 0, timeout: 1)
160
- expect(dropdown.options.map(&:log)).to eq([[[:click]], [[:click]], [[:click]]])
161
- end
162
- end
163
-
164
- describe 'actions' do
165
- it 'hovers with move_to once the element is stable' do
166
- menu = element
167
- driver.script({ id: 'menu' }, menu)
168
- page.hover({ id: 'menu' }, timeout: 1)
169
- expect(driver.actions).to eq([[:move_to, menu], [:perform]])
170
- end
171
-
172
- it 'drags the source onto the target' do
173
- source = element
174
- target = element
175
- driver.script({ id: 'src' }, source).script({ id: 'tgt' }, target)
176
- expect(page.drag_and_drop({ id: 'src' }, { id: 'tgt' }, timeout: 1)).to eq(source)
177
- expect(driver.actions).to eq([[:drag_and_drop, source, target], [:perform]])
178
- end
179
-
180
- it 'scrolls a hidden but present element into view' do
181
- below = element(displayed: false)
182
- driver.script({ id: 'below' }, below)
183
- page.scroll_into_view({ id: 'below' }, timeout: 1)
184
- expect(driver.actions).to eq([[:scroll_to, below], [:perform]])
185
- end
186
- end
187
-
188
- describe 'readers' do
189
- it 'answer for a present element, hidden included' do
190
- driver.script({ id: 'x' }, element(displayed: false, text: 'Hello', attributes: { 'href' => '/a' }))
191
- expect(page.text({ id: 'x' }, timeout: 1)).to eq('Hello')
192
- expect(page.attribute({ id: 'x' }, 'href', timeout: 1)).to eq('/a')
193
- expect(page.displayed?({ id: 'x' }, timeout: 1)).to be(false)
194
- expect(page.enabled?({ id: 'x' }, timeout: 1)).to be(true)
195
- expect(page.selected?({ id: 'x' }, timeout: 1)).to be(false)
196
- end
197
-
198
- it 'raise the timeout error when nothing matches' do
199
- expect { page.text({ id: 'none' }, timeout: 0.2) }.to raise_error(WaitHelper::TimeoutError)
200
- expect { page.displayed?({ id: 'none' }, timeout: 0.2) }.to raise_error(WaitHelper::TimeoutError)
201
- end
202
- end
203
-
204
- describe 'braceless locators' do
205
- it 'treat keywords as the locator' do
206
- driver.script({ css: '.x' }, element(tag_name: 'span')).script({ id: 'y' }, element).script({ id: 'z' }, element)
207
- expect(page.find_element(css: '.x').tag_name).to eq('span')
208
- page.click(id: 'y')
209
- expect(page.text(id: 'z')).to eq('hi')
210
- expect(driver.finds.uniq).to eq([{ css: '.x' }, { id: 'y' }, { id: 'z' }])
211
- end
212
- end
213
-
214
- describe 'timeout errors' do
215
- it 'subclass Selenium TimeoutError, describe the wait and start the backtrace at the caller' do
216
- driver.script({ id: 'x' }, element(displayed: false))
217
- expect { page.send_keys({ id: 'x' }, 'a', timeout: 0.3) }.to raise_error(WaitHelper::TimeoutError) { |error|
218
- expect(error).to be_a(Selenium::WebDriver::Error::TimeoutError)
219
- expect(error.message)
220
- .to eq('Waited 0.3s for {id: "x"} to be visible and enabled; last seen: present but not visible')
221
- expect(error.backtrace.first).to include(File.basename(__FILE__))
222
- expect(error.backtrace.first).not_to include('wait_helper.rb')
223
- }
224
- end
225
- end
226
-
227
- describe '#eventually' do
228
- it 'returns the block value after two failing tries' do
229
- tries = 0
230
- result = page.eventually(timeout: 1) do
231
- tries += 1
232
- expect(tries).to eq(3)
233
- :done
234
- end
235
- expect(result).to eq(:done)
236
- end
237
-
238
- it 're-raises the last expectation failure with the still-failing prefix' do
239
- answer = 1
240
- expect { page.eventually(timeout: 0.3) { expect(answer).to eq(2) } }
241
- .to raise_error(RSpec::Expectations::ExpectationNotMetError) { |error|
242
- expect(error.message).to start_with('(still failing after 0.3s)').and(include('expected: 2'))
243
- }
244
- end
245
-
246
- it 'raises any other error on the first try' do
247
- tries = 0
248
- expect { page.eventually(timeout: 1) { raise "boom #{tries += 1}" } }.to raise_error(RuntimeError, 'boom 1')
249
- end
250
-
251
- it 'stretches a nested call without timeout: to what is left of its own deadline' do
252
- allow(WaitHelper).to receive(:default_timeout).and_return(1)
253
- driver.script({ css: '.x' }, *Array.new(15), element(text: 'y'))
254
- tries = 0
255
- started = clock
256
- page.eventually(timeout: 3) do
257
- tries += 1
258
- expect(page.text(css: '.x')).to eq 'y'
259
- end
260
- expect([tries, clock - started]).to match([1, be_between(1.3, 2.9)])
261
- end
262
-
263
- it 'retries a nested timeout: per attempt and fails at its own deadline with the prefix' do
264
- started = clock
265
- expect { page.eventually(timeout: 1) { page.text({ css: '.never' }, timeout: 0.2) } }
266
- .to raise_error(WaitHelper::TimeoutError, /\A\(still failing after 1s\)\nWaited [\d.]+s for \{css: "\.never"\}/)
267
- expect(clock - started).to be_between(0.9, 1.4)
268
- expect(driver.finds.size).to be > 5
269
- end
270
-
271
- it 'caps a nested action at its own deadline' do
272
- started = clock
273
- expect { page.eventually(timeout: 0.5) { page.click({ id: 'never' }, timeout: 5) } }
274
- .to raise_error(WaitHelper::TimeoutError)
275
- expect(clock - started).to be < 0.8
276
- end
277
- end
278
-
279
- describe RaiderElement do
280
- it 'chains finders, waiting at each level inside the parent' do
281
- email = element
282
- form = element.script({ id: 'email' }, nil, email)
283
- driver.script({ css: '.form' }, nil, form)
284
- page.find_element(css: '.form').find_element(id: 'email').send_keys('a')
285
- expect(driver.finds).to eq([{ css: '.form' }] * 2)
286
- expect(form.finds).to eq([{ id: 'email' }] * 2)
287
- expect(email.log).to eq([[:send_keys, 'a']])
288
- end
289
-
290
- it 'returns an Array of wrappers from find_elements' do
291
- driver.script_many({ css: 'li' }, [element, element])
292
- expect(page.find_elements(css: 'li')).to all(be_a(described_class))
293
- end
294
-
295
- it 'delegates unwrapped methods to the raw element and compares by it' do
296
- raw = element(tag_name: 'a')
297
- driver.script({ id: 'x' }, raw)
298
- wrapper = page.find_element(id: 'x')
299
- other = page.find_element(id: 'x')
300
- expect([wrapper.tag_name, wrapper.rect, wrapper.respond_to?(:tag_name)]).to eq(['a', [0, 0, 10, 10], true])
301
- expect(wrapper.native).to be(raw)
302
- expect([wrapper == raw, wrapper == other, [wrapper, other].uniq.size]).to eq([true, true, 1])
303
- expect(wrapper).not_to be_a(Selenium::WebDriver::Element)
304
- end
305
-
306
- it 're-finds a stale wrapper through its scope' do
307
- first = element
308
- second = element
309
- driver.script({ id: 'x' }, first, second)
310
- wrapper = page.find_element(id: 'x')
311
- first.go_stale
312
- wrapper.click(timeout: 1)
313
- expect([first.log, second.log, wrapper.native]).to eq([[], [[:click]], second])
314
- end
315
-
316
- it 'keeps its element after a timed-out action' do
317
- raw = element(tag_name: 'a')
318
- driver.script({ id: 'x' }, raw, nil)
319
- wrapper = page.find_element(id: 'x')
320
- hash = wrapper.hash
321
- raw.go_hidden
322
- expect { wrapper.click(timeout: 0.2) }.to raise_error(WaitHelper::TimeoutError)
323
- expect([wrapper.native, wrapper.tag_name, wrapper == raw, wrapper.hash]).to eq([raw, 'a', true, hash])
324
- end
325
-
326
- it 'heals a chain when the parent was re-rendered' do
327
- old_button = element
328
- new_button = element
329
- old_form = element.script({ id: 'go' }, old_button)
330
- new_form = element.script({ id: 'go' }, new_button)
331
- driver.script({ css: '.form' }, old_form, new_form)
332
- button = page.find_element(css: '.form').find_element(id: 'go')
333
- [old_form, old_button].each(&:go_stale)
334
- button.click(timeout: 1)
335
- expect(new_button.log).to eq([[:click]])
336
- end
337
-
338
- it 'raises a stale find_elements member with a re-query hint' do
339
- item = element
340
- driver.script_many({ css: 'li' }, [item])
341
- member = page.find_elements(css: 'li').first
342
- item.go_stale
343
- expect { member.click(timeout: 1) }.to raise_error(errors::StaleElementReferenceError, /find_elements again/)
344
- end
345
-
346
- context 'when a parent that cannot be found again goes stale' do
347
- let(:raw_row) { element.script({ css: 'td' }, element) }
348
- let(:hint) { /find_elements again/ }
349
-
350
- def stale_row
351
- driver.script_many({ css: 'tr' }, [raw_row])
352
- row = page.find_elements(css: 'tr').first
353
- raw_row.go_stale
354
- row
355
- end
356
-
357
- def at_once
358
- started = clock
359
- yield
360
- expect(clock - started).to be < 0.3
361
- end
362
-
363
- it 'raises from find_elements at timeout: 0 instead of answering []' do
364
- row = stale_row
365
- expect { row.find_elements({ css: '.err' }, timeout: 0) }.to raise_error(errors::StaleElementReferenceError, hint)
366
- end
367
-
368
- it 'raises from find_elements at once instead of waiting and answering []' do
369
- row = stale_row
370
- at_once { expect { row.find_elements(css: 'td', timeout: 2) }.to raise_error(errors::StaleElementReferenceError, hint) }
371
- end
372
-
373
- it 'raises from find_element at once with the hint' do
374
- row = stale_row
375
- at_once { expect { row.find_element(css: 'td', timeout: 2) }.to raise_error(errors::StaleElementReferenceError, hint) }
376
- end
377
-
378
- it 'raises for a parent passed in as an element, suggesting a locator' do
379
- parent = page.find_element(raw_row)
380
- raw_row.go_stale
381
- at_once do
382
- expect { parent.find_element(css: 'td', timeout: 2) }
383
- .to raise_error(errors::StaleElementReferenceError, /pass .*locator/)
384
- end
385
- end
386
-
387
- it 'raises at once for a stale ancestor further up the chain' do
388
- leaf = element
389
- middle = element.script({ css: 'td' }, leaf)
390
- raw_row.script({ css: '.cell' }, middle)
391
- driver.script_many({ css: 'tr' }, [raw_row])
392
- button = page.find_elements(css: 'tr').first.find_element(css: '.cell').find_element(css: 'td')
393
- [raw_row, middle, leaf].each(&:go_stale)
394
- at_once { expect { button.click(timeout: 2) }.to raise_error(errors::StaleElementReferenceError, hint) }
395
- end
396
-
397
- it 'still re-finds a stale child inside a live parent' do
398
- old_button = element
399
- new_button = element
400
- raw_row.script({ css: 'button' }, old_button, new_button)
401
- driver.script_many({ css: 'tr' }, [raw_row])
402
- button = page.find_elements(css: 'tr').first.find_element(css: 'button')
403
- old_button.go_stale
404
- button.click(timeout: 1)
405
- expect(new_button.log).to eq([[:click]])
406
- end
407
- end
408
-
409
- it 'passes both raw elements to drag_and_drop' do
410
- source = element
411
- target = element
412
- driver.script({ id: 'src' }, source).script({ id: 'tgt' }, target)
413
- page.find_element(id: 'src').drag_and_drop(page.find_element(id: 'tgt'))
414
- step = driver.actions.first
415
- expect([step[1].equal?(source), step[2].equal?(target)]).to eq([true, true])
416
- end
417
-
418
- it 'adds exactly the planned public methods' do
419
- expect(described_class.public_instance_methods(false).sort)
420
- .to eq(%i[== attribute clear click displayed? drag_and_drop enabled? eql? find_element find_elements hash
421
- hover native scroll_into_view select_by selected? send_keys submit text])
422
- end
423
- end
424
-
425
- describe 'the choke point' do
426
- let(:actions) { [] }
427
-
428
- before do
429
- dropdown = WaitHelperSpec::FakeSelect.new
430
- dropdown.script({ css: 'y' }, element).script_many({ css: 'y' }, [element])
431
- driver.script({ id: 'x' }, dropdown)
432
- allow_any_instance_of(WaitHelper::Waiter).to receive(:perform_action).and_wrap_original do |original, *args, **options, &block|
433
- actions << args.first
434
- original.call(*args, **options, &block)
435
- end
436
- end
437
-
438
- def calls(locator)
439
- { find_element: [locator], find_elements: [locator], click: [locator], clear: [locator], submit: [locator],
440
- hover: [locator], scroll_into_view: [locator], text: [locator], displayed?: [locator],
441
- enabled?: [locator], selected?: [locator], send_keys: [locator, 'a'], select_by: [locator, :text, 'Blue'],
442
- attribute: [locator, 'href'], drag_and_drop: [locator, locator] }
443
- end
444
-
445
- it 'is called once per mixin method, with the method name' do
446
- calls({ id: 'x' }).each do |name, args|
447
- actions.clear
448
- page.public_send(name, *args, timeout: 1)
449
- expect(actions).to eq([name])
450
- end
451
- end
452
-
453
- it 'is called once per wrapper method, with the method name' do
454
- wrapper = page.find_element(id: 'x')
455
- calls({ css: 'y' }).each do |name, args|
456
- args = args.drop(1) unless %i[find_element find_elements drag_and_drop].include?(name)
457
- args = [wrapper] if name == :drag_and_drop
458
- actions.clear
459
- wrapper.public_send(name, *args, timeout: 1)
460
- expect(actions).to eq([name])
461
- end
462
- end
463
- end
464
-
465
- describe 'footprint' do
466
- it 'adds exactly the public API, with no private methods and nothing named wait' do
467
- expect(WaitHelper.public_instance_methods(false).sort)
468
- .to eq(%i[attribute clear click displayed? drag_and_drop enabled? eventually find_element find_elements hover
469
- scroll_into_view select_by selected? send_keys submit text])
470
- expect(WaitHelper.private_instance_methods(false)).to be_empty
471
- expect(page.respond_to?(:wait, true)).to be(false)
472
- end
473
- end
474
-
475
- describe '.observer' do
476
- # Records every hook call; tokens are the call's index.
477
- let(:observer) do
478
- Class.new do
479
- attr_reader :calls
480
-
481
- def initialize = @calls = []
482
-
483
- %i[action_started group_started].each do |hook|
484
- define_method(hook) do |**details|
485
- @calls << [hook, details]
486
- @calls.size - 1
487
- end
488
- end
489
-
490
- %i[action_ready action_finished group_retried group_finished].each do |hook|
491
- define_method(hook) do |token, **details|
492
- @calls << [hook, token, details]
493
- end
494
- end
495
- end.new
496
- end
497
-
498
- around do |example|
499
- WaitHelper.observer = observer
500
- example.run
501
- ensure
502
- WaitHelper.observer = nil
503
- end
504
-
505
- def started = observer.calls.select { |call| call.first == :action_started }.map(&:last)
506
- def finished = observer.calls.select { |call| call.first == :action_finished }.map(&:last)
507
-
508
- it 'reports a click with its locator, source line, waited time and the ready element' do
509
- button = element
510
- driver.script({ id: 'go' }, nil, button)
511
- line = __LINE__ + 1
512
- page.click({ id: 'go' }, timeout: 1)
513
-
514
- expect(observer.calls.map(&:first)).to eq(%i[action_started action_ready action_finished])
515
- expect(started.first).to include(action: :click, locator: { strategy: 'id', selector: 'go' }, within: [],
516
- source: { file: Pathname(__FILE__).relative_path_from(Dir.pwd).to_s, line: line })
517
- .and(satisfy { |details| !details.key?(:label) })
518
- expect(observer.calls[2][1]).to eq(0)
519
- expect(finished.first).to include(status: :passed, retries: 2, error: nil, element: button)
520
- expect(finished.first[:waited_ms]).to be_between(50, 500)
521
- end
522
-
523
- it 'reports the keys of send_keys and how and what of select_by as args, and nil args for other actions' do
524
- driver.script({ id: 'user' }, element)
525
- driver.script({ id: 'colour' }, WaitHelperSpec::FakeSelect.new)
526
- page.send_keys({ id: 'user' }, 'admin', :enter, timeout: 1)
527
- page.select_by({ id: 'colour' }, :text, 'Blue', timeout: 1)
528
- page.click({ id: 'user' }, timeout: 1)
529
-
530
- expect(started.map { |event| event.values_at(:action, :args) })
531
- .to eq([[:send_keys, ['admin', :enter]], [:select_by, [:text, 'Blue']], [:click, nil]])
532
- end
533
-
534
- it 'reports action_ready with the ready element before the action block runs' do
535
- field = element
536
- driver.script({ id: 'user' }, nil, field)
537
- log_when_ready = []
538
- allow(observer).to receive(:action_ready).and_wrap_original do |original, token, **details|
539
- log_when_ready << details[:element].log.dup
540
- original.call(token, **details)
541
- end
542
- page.send_keys({ id: 'user' }, 'admin', timeout: 1)
543
-
544
- expect(observer.calls.map(&:first)).to eq(%i[action_started action_ready action_finished])
545
- expect(observer.calls[1]).to eq([:action_ready, 0, { element: field }])
546
- expect(log_when_ready).to eq([[]])
547
- expect(field.log).to eq([[:send_keys, 'admin']])
548
- end
549
-
550
- it 'reports a chained find with within, outermost first' do
551
- driver.script({ css: '.form' }, element.script({ tag_name: 'td' }, element.script({ id: 'email' }, element)))
552
- page.find_element(css: '.form').find_element(tag_name: 'td').find_element(id: 'email')
553
-
554
- expect(started.last).to include(action: :find_element, locator: { strategy: 'id', selector: 'email' },
555
- within: [{ strategy: 'css', selector: '.form' }, { strategy: 'tag_name', selector: 'td' }])
556
- end
557
-
558
- it 'reports a timed-out action as failed with its error, then re-raises' do
559
- driver.script({ id: 'gone' }, nil)
560
- expect { page.click({ id: 'gone' }, timeout: 0.3) }.to raise_error(WaitHelper::TimeoutError)
561
-
562
- expect(finished.first).to include(status: :failed, element: nil)
563
- expect(finished.first[:error]).to be_a(WaitHelper::TimeoutError)
564
- expect(finished.first[:retries]).to be >= 2
565
- end
566
-
567
- it 'reports eventually as a group with the retries it took' do
568
- tries = 0
569
- page.eventually(timeout: 2) { expect(tries += 1).to eq(3) }
570
-
571
- group = observer.calls.find { |call| call.first == :group_started }.last
572
- expect(group).to include(action: :eventually)
573
- expect(group[:source]).to include(line: be_a(Integer))
574
- expect(observer.calls.last).to match([:group_finished, 0, { status: :passed, retries: 2, error: nil }])
575
- end
576
-
577
- it 'reports group_retried with the group token and the earlier error before each new pass of eventually' do
578
- tries = 0
579
- page.eventually(timeout: 2) { expect(tries += 1).to eq(3) }
580
-
581
- retried = observer.calls.select { |call| call.first == :group_retried }
582
- expect(retried.map { |call| call[1] }).to eq([0, 0])
583
- expect(retried.map { |call| call.last[:error] }).to all(be_a(RSpec::Expectations::ExpectationNotMetError))
584
- expect(observer.calls.map(&:first)).to eq(%i[group_started group_retried group_retried group_finished])
585
- end
586
-
587
- it 'reports a failing eventually as failed with its error' do
588
- tries = 0
589
- expect { page.eventually(timeout: 0.2) { expect(tries += 1).to eq(0) } }.to raise_error(RSpec::Expectations::ExpectationNotMetError)
590
- expect(observer.calls.last.last).to include(status: :failed, error: be_a(RSpec::Expectations::ExpectationNotMetError))
591
- end
592
-
593
- it 'normalises class to class_name and link to link_text' do
594
- aliases = { css_selector: 'css', class: 'class_name', link: 'link_text', partial_link: 'partial_link_text', name: 'name' }
595
- aliases.each_key do |strategy|
596
- driver.script({ strategy => 'x' }, element)
597
- page.displayed?({ strategy => 'x' }, timeout: 0)
598
- end
599
-
600
- expect(started.map { |event| event[:locator][:strategy] }).to eq(aliases.values)
601
- end
602
-
603
- it 'never takes the source from a file in helpers/, such as the trace helper' do
604
- # The given path of wait_helper.rb, and the real path of raider_element.rb (via __dir__).
605
- given, real = WaitHelper::Waiter::HELPER_FILES.map { |file| File.dirname(file) }
606
-
607
- expect(WaitHelper::Observation.project?(File.join(given, 'trace_helper.rb'))).to be(false)
608
- expect(WaitHelper::Observation.project?(File.join(real, 'driver_helper.rb'))).to be(false)
609
- expect(WaitHelper::Observation.project?(File.expand_path('spec/login_spec.rb'))).to be(true)
610
- end
611
-
612
- it 'reports no label for an action called from a page_objects method' do
613
- Dir.mktmpdir do |dir|
614
- FileUtils.mkdir_p(File.join(dir, 'page_objects'))
615
- path = File.join(dir, 'page_objects', 'login.rb')
616
- File.write(path, "def login_button(page) = page.click({ id: 'go' }, timeout: 1)\n")
617
- load path
618
- driver.script({ id: 'go' }, element)
619
- login_button(page)
620
- end
621
-
622
- expect(started.first).not_to have_key(:label)
623
- expect(started.first[:source][:file]).to end_with('page_objects/login.rb')
624
- end
625
-
626
- it 'drops the observer, warns once, and still clicks when working out the source raises' do
627
- button = element
628
- driver.script({ id: 'go' }, button)
629
- allow(Dir).to receive(:pwd).and_raise(Errno::ENOENT)
630
-
631
- expect { 2.times { page.click({ id: 'go' }, timeout: 1) } }.to output(/\A\[trace\] observer stopped: Errno::ENOENT.*\n\z/).to_stderr
632
- expect(button.log).to eq([%i[click]] * 2)
633
- expect(WaitHelper.observer).to be_nil
634
- end
635
-
636
- it 'drops an observer that raises, warns once, and still clicks' do
637
- allow(observer).to receive(:action_started).and_raise('boom')
638
- button = element
639
- driver.script({ id: 'go' }, button)
640
-
641
- expect { 2.times { page.click({ id: 'go' }, timeout: 1) } }.to output(/\A\[trace\] observer stopped: RuntimeError: boom\n\z/).to_stderr
642
- expect(button.log).to eq([%i[click]] * 2)
643
- expect(WaitHelper.observer).to be_nil
644
- end
645
-
646
- it 'still prints observer stopped under -W0, where Kernel#warn is silent' do
647
- allow(observer).to receive(:action_started).and_raise('boom')
648
- driver.script({ id: 'go' }, element)
649
- verbose = $VERBOSE
650
- $VERBOSE = nil
651
-
652
- expect { page.click({ id: 'go' }, timeout: 1) }.to output("[trace] observer stopped: RuntimeError: boom\n").to_stderr
653
- ensure
654
- $VERBOSE = verbose
655
- end
656
- end
657
-
658
- describe '.default_timeout' do
659
- around do |example|
660
- Dir.mktmpdir do |dir|
661
- Dir.chdir(dir) { example.run }
662
- ensure
663
- WaitHelper.instance_variable_set(:@default_timeout, nil)
664
- end
665
- end
666
-
667
- it 'is 10 without config/config.yml' do
668
- WaitHelper.instance_variable_set(:@default_timeout, nil)
669
- expect(WaitHelper.default_timeout).to eq(10)
670
- end
671
-
672
- it 'reads timeout from config/config.yml' do
673
- WaitHelper.instance_variable_set(:@default_timeout, nil)
674
- Dir.mkdir('config')
675
- File.write('config/config.yml', YAML.dump('timeout' => 3))
676
- expect(WaitHelper.default_timeout).to eq(3)
677
- end
678
- end
679
- end