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,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/expectations'
4
+ require_relative '../runtime'
5
+ require_relative 'matchers/base'
6
+ require_relative 'matchers/element'
7
+ require_relative 'matchers/page'
8
+ require_relative 'matchers/presence'
9
+ require_relative 'matchers/retrying_block'
10
+
11
+ module Raider
12
+ # Web-first RSpec matchers that retry until the page settles.
13
+ # rubocop:disable Naming/PredicatePrefix -- the matcher names are RSpec's vocabulary
14
+ module Matchers
15
+ def have_text(text, exact: false, wait: nil)
16
+ Element::HaveText.new(text, exact: exact, wait: wait)
17
+ end
18
+
19
+ def have_value(value, wait: nil)
20
+ Element::HaveValue.new(value, wait: wait)
21
+ end
22
+
23
+ def have_attribute(name, value = Element::HaveAttribute::PRESENT, wait: nil)
24
+ Element::HaveAttribute.new(name, value, wait: wait)
25
+ end
26
+
27
+ def be_visible(wait: nil)
28
+ Element::BeVisible.new(wait: wait)
29
+ end
30
+
31
+ def be_hidden(wait: nil)
32
+ Element::BeHidden.new(wait: wait)
33
+ end
34
+
35
+ def be_enabled(wait: nil)
36
+ Element::BeEnabled.new(wait: wait)
37
+ end
38
+
39
+ def be_disabled(wait: nil)
40
+ Element::BeDisabled.new(wait: wait)
41
+ end
42
+
43
+ def be_checked(wait: nil)
44
+ Element::BeChecked.new(wait: wait)
45
+ end
46
+
47
+ def have_element(locator = nil, wait: nil, **by)
48
+ raise ArgumentError, 'have_element takes a locator, not both a hash and keywords' if locator && !by.empty?
49
+ raise ArgumentError, 'have_element needs a locator' if locator.nil? && by.empty?
50
+
51
+ Presence::HaveElement.new(locator || by, wait: wait)
52
+ end
53
+
54
+ def have_css(selector, wait: nil)
55
+ have_element({ css: selector }, wait: wait)
56
+ end
57
+
58
+ def have_xpath(selector, wait: nil)
59
+ have_element({ xpath: selector }, wait: wait)
60
+ end
61
+
62
+ def have_count(count, wait: nil)
63
+ Page::HaveCount.new(count, wait: wait)
64
+ end
65
+
66
+ def have_title(expected, wait: nil)
67
+ Page::HaveTitle.new(expected, wait: wait)
68
+ end
69
+
70
+ def have_url(expected, wait: nil)
71
+ Page::HaveUrl.new(expected, wait: wait)
72
+ end
73
+ end
74
+ # rubocop:enable Naming/PredicatePrefix
75
+ end
76
+
77
+ target = RSpec::Expectations::BlockExpectationTarget
78
+ target.prepend(Raider::Matchers::RetryingBlock) unless target.ancestors.include?(Raider::Matchers::RetryingBlock)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'config'
4
+
5
+ module Raider
6
+ # Builds the XPath and CSS strings behind the label, placeholder, alt,
7
+ # title, text and test_id locator keys. Every caller-supplied value passes
8
+ # through xpath_literal or the CSS escaper; nothing else is interpolated.
9
+ module Selectors
10
+ ATTRIBUTE_NAME = /\A[A-Za-z_][\w-]*\z/
11
+ LABEL_CONTROLS = %w[input textarea select button meter output progress]
12
+ .map { |tag| "self::#{tag}" }.join(' or ').freeze
13
+ EXCLUDED_TEXT = 'not(self::script or self::style or self::noscript or self::template or self::title or self::head) ' \
14
+ 'and not(ancestor::head)'
15
+
16
+ module_function
17
+
18
+ def xpath_literal(string)
19
+ value = string.to_s
20
+ return "'#{value}'" unless value.include?("'")
21
+ return %("#{value}") unless value.include?('"')
22
+
23
+ parts = value.split("'", -1).map { |part| "'#{part}'" }
24
+ "concat(#{parts.join(%q(, "'", ))})"
25
+ end
26
+
27
+ def label(value, exact:)
28
+ literal = xpath_literal(value)
29
+ [
30
+ ".//*[@id = //label[#{match('.', literal, exact)}]/@for]",
31
+ ".//label[#{match('.', literal, exact)}]//*[(#{LABEL_CONTROLS}) and not(self::input[@type='hidden'])]",
32
+ ".//*[#{match('@aria-label', literal, exact)}]",
33
+ ".//*[@aria-labelledby and #{match('string(id(@aria-labelledby))', literal, exact)}]"
34
+ ].join(' | ')
35
+ end
36
+
37
+ def text(value, exact:)
38
+ matcher = match('.', xpath_literal(value), exact)
39
+ candidate = "#{EXCLUDED_TEXT} and #{matcher}"
40
+ ".//*[#{candidate} and not(.//*[#{candidate}])]"
41
+ end
42
+
43
+ def attribute_css(name, value, exact:)
44
+ raise ArgumentError, "invalid attribute name: #{name}" unless name.to_s.match?(ATTRIBUTE_NAME)
45
+
46
+ escaped = value.to_s.gsub('\\') { '\\\\' }.gsub('"') { '\\"' }
47
+ "[#{name}#{exact ? '=' : '*='}\"#{escaped}\"]"
48
+ end
49
+
50
+ def test_id(value)
51
+ attribute_css(Raider.test_id_attribute, value, exact: true)
52
+ end
53
+
54
+ def match(expression, literal, exact)
55
+ return "normalize-space(#{expression}) = #{literal}" if exact
56
+
57
+ "contains(normalize-space(#{expression}), #{literal})"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+ require_relative 'config'
5
+ require_relative 'errors'
6
+ require_relative 'context'
7
+ require_relative 'finders'
8
+ require_relative 'observer'
9
+
10
+ module Raider
11
+ # Renders a find_element argument list for error messages.
12
+ module Locator
13
+ module_function
14
+
15
+ def describe(args)
16
+ how, what = args
17
+ args.size == 1 && how.is_a?(Hash) ? from_hash(how) : "{ #{how.to_s.inspect}: #{what.inspect} }"
18
+ rescue StandardError
19
+ args.inspect
20
+ end
21
+
22
+ def from_hash(hash)
23
+ key, value = hash.first
24
+ return "by_role(#{role_arguments(value)})" if key == :role && value.is_a?(Array)
25
+
26
+ "{ #{key}: #{plain_arguments(value)} }"
27
+ end
28
+
29
+ def role_arguments(value)
30
+ name, options = value
31
+ ["'#{name}'", *render_options(options)].join(', ')
32
+ end
33
+
34
+ def plain_arguments(value)
35
+ return value.inspect unless value.is_a?(Array)
36
+
37
+ text, options = value
38
+ [text.inspect, *render_options(options)].join(', ')
39
+ end
40
+
41
+ def render_options(options)
42
+ return [] unless options.is_a?(Hash)
43
+
44
+ options.map { |key, option| "#{key}: #{option.inspect}" }
45
+ end
46
+ end
47
+
48
+ # Prepended to SearchContext: find_element must match exactly one element, and
49
+ # both finds wait for a match.
50
+ module StrictFinder
51
+ def find_element(*args)
52
+ Finders.ensure_registered
53
+ attempt = element_attempt(args) { super(*args) }
54
+ Observer.stamp(await_element(args, attempt), args, self)
55
+ end
56
+
57
+ def find_elements(*args)
58
+ return Observer.stamp_all(super, args, self) if Wait.immediate?
59
+
60
+ attempt = elements_attempt(args) { super(*args) }
61
+ found = []
62
+ begin
63
+ Wait.until(timeout: Raider.timeout) { (found = attempt.call).any? }
64
+ rescue TimeoutError
65
+ nil
66
+ end
67
+ Observer.stamp_all(found, args, self)
68
+ end
69
+
70
+ private
71
+
72
+ def await_element(args, attempt)
73
+ return attempt.call || raise(Selenium::WebDriver::Error::NoSuchElementError, not_found(args)) if Wait.immediate?
74
+
75
+ started = clock
76
+ begin
77
+ Wait.until(timeout: Raider.timeout) { attempt.call }
78
+ rescue TimeoutError
79
+ raise Selenium::WebDriver::Error::NoSuchElementError,
80
+ "#{not_found(args)} (waited #{Describe.seconds(clock - started)}s)"
81
+ end
82
+ end
83
+
84
+ # A callable making one attempt at the element: the element, or nil when nothing
85
+ # matched yet. The context is decided here, once per find, not per attempt.
86
+ def element_attempt(args, &raw)
87
+ if RoleFinder.role_args?(args)
88
+ queried = role_attempt(args)
89
+ strict = Raider.strict
90
+ return -> { sole(args, queried.call, strict) }
91
+ end
92
+ return -> { native_or_nil(&raw) } unless Raider.strict && Context.web?(bridge)
93
+
94
+ -> { sole(args, Wait.immediately { find_elements(*args) }, true) }
95
+ end
96
+
97
+ def elements_attempt(args, &raw)
98
+ RoleFinder.role_args?(args) ? role_attempt(args) : raw
99
+ end
100
+
101
+ # Validates the role locator and checks the context once, then queries the engine directly.
102
+ def role_attempt(args)
103
+ role, options = RoleFinder.parse(args)
104
+ raise NotSupported, RoleFinder::NATIVE_MESSAGE unless Context.web?(bridge)
105
+
106
+ scope = is_a?(Selenium::WebDriver::Driver) ? nil : self
107
+ -> { RoleQuery.run(bridge, scope, role, options) }
108
+ end
109
+
110
+ def sole(args, found, strict)
111
+ raise AmbiguousMatch.new(Locator.describe(args), found) if strict && found.size > 1
112
+
113
+ found.first
114
+ end
115
+
116
+ def native_or_nil
117
+ yield
118
+ rescue Selenium::WebDriver::Error::NoSuchElementError
119
+ nil
120
+ end
121
+
122
+ def not_found(args)
123
+ "no such element: #{Locator.describe(args)}"
124
+ end
125
+
126
+ def clock
127
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
128
+ end
129
+ end
130
+ end
131
+
132
+ # Selenium aliases `first` to find_element on Driver and Element, which
133
+ # bypasses the prepend above; `find_elements(...).first` stays the non-strict escape.
134
+ module Raider
135
+ # Routes Selenium's `first` alias through the strict find_element.
136
+ module StrictFirst
137
+ def first(*args) = find_element(*args)
138
+ end
139
+ end
140
+
141
+ Selenium::WebDriver::SearchContext.prepend(Raider::StrictFinder)
142
+ [Selenium::WebDriver::Driver, Selenium::WebDriver::Element].each { |klass| klass.prepend(Raider::StrictFirst) }
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+ require_relative 'config'
5
+ require_relative 'errors'
6
+
7
+ module Raider
8
+ # The runtime wait loop. Nested waits share one deadline through DEADLINE_KEY
9
+ # (the same key the generated wait helper uses).
10
+ module Wait
11
+ DEADLINE_KEY = :raider_wait_deadline
12
+ # Like DEADLINE_KEY, fiber-scoped on purpose (Thread.current[] is per fiber).
13
+ IMMEDIATE_KEY = :raider_wait_immediate
14
+
15
+ class << self
16
+ def deadline
17
+ Thread.current[DEADLINE_KEY]
18
+ end
19
+
20
+ # Runs the block with finds making one attempt each (the caller does the waiting).
21
+ def immediately
22
+ previous = Thread.current[IMMEDIATE_KEY]
23
+ Thread.current[IMMEDIATE_KEY] = true
24
+ yield
25
+ ensure
26
+ Thread.current[IMMEDIATE_KEY] = previous
27
+ end
28
+
29
+ def immediate?
30
+ Thread.current[IMMEDIATE_KEY] == true
31
+ end
32
+
33
+ # Polls until the block returns a truthy value. The block may
34
+ # `throw :not_ready, reason` to say why it is not ready yet.
35
+ def until(timeout: nil, interval: 0.1)
36
+ limit, = limits(timeout)
37
+ within(limit) do
38
+ reason = nil
39
+ loop do
40
+ result = nil
41
+ thrown = catch(:not_ready) do
42
+ result = yield
43
+ nil
44
+ end
45
+ return result if result
46
+
47
+ reason = thrown || reason
48
+ raise TimeoutError, reason || 'condition not met' if now >= limit
49
+
50
+ pause(limit, interval)
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # Inside an enclosing deadline, a call without timeout waits for what is left;
58
+ # one with timeout waits that long, never past the enclosing deadline.
59
+ def limits(timeout)
60
+ started = now
61
+ enclosing = deadline
62
+ own = started + (timeout || Raider.timeout) if timeout || !enclosing
63
+ limit = [own, enclosing].compact.min
64
+ [limit, limit - started]
65
+ end
66
+
67
+ def within(limit)
68
+ enclosing = deadline
69
+ Thread.current[DEADLINE_KEY] = limit
70
+ yield
71
+ ensure
72
+ Thread.current[DEADLINE_KEY] = enclosing
73
+ end
74
+
75
+ def pause(limit, interval)
76
+ sleep([[interval, limit - now].min, 0].max)
77
+ end
78
+
79
+ def now
80
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'selenium-webdriver'
4
+ require_relative 'runtime/config'
5
+ require_relative 'runtime/errors'
6
+ require_relative 'runtime/by_role'
7
+ require_relative 'runtime/wait'
8
+ require_relative 'runtime/context'
9
+ require_relative 'runtime/selectors'
10
+ require_relative 'runtime/locator_converter'
11
+ require_relative 'runtime/finders'
12
+ require_relative 'runtime/role_finder'
13
+ require_relative 'runtime/observer'
14
+ require_relative 'runtime/actionability'
15
+ require_relative 'runtime/strict'
16
+ require_relative 'runtime/actionable'
17
+
18
+ Raider::Finders.check_api
19
+ Raider::Finders.install
20
+ Raider::Finders.guard
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NameNormalizer
4
- SUFFIXES = %w[_page _spec _steps _helper _test _feature].freeze
4
+ SUFFIXES = %w[_page _spec _helper _test].freeze
5
5
 
6
6
  module_function
7
7
 
@@ -4,21 +4,13 @@ require 'yaml'
4
4
 
5
5
  module ScaffoldProjectDetector
6
6
  AUTOMATION_GEMS = {
7
- 'watir' => 'watir',
8
7
  'selenium-webdriver' => 'selenium',
9
8
  'appium_lib' => 'appium',
10
9
  'eyes_selenium' => 'selenium',
11
- 'axe-core-rspec' => 'selenium',
12
- 'axe-core-cucumber' => 'selenium'
13
- }.freeze
14
-
15
- FRAMEWORK_GEMS = {
16
- 'rspec' => 'rspec',
17
- 'cucumber' => 'cucumber'
10
+ 'axe-core-rspec' => 'selenium'
18
11
  }.freeze
19
12
 
20
13
  CONFIG_PATH = 'config/config.yml'
21
- WAIT_HELPER_PATH = 'helpers/wait_helper.rb'
22
14
 
23
15
  class Error < StandardError; end
24
16
 
@@ -28,9 +20,7 @@ module ScaffoldProjectDetector
28
20
  gemfile = read_gemfile
29
21
  {
30
22
  automation: detect_automation(gemfile),
31
- framework: detect_framework(gemfile),
32
- has_spec: Dir.exist?('spec'),
33
- has_features: Dir.exist?('features')
23
+ has_spec: Dir.exist?('spec')
34
24
  }
35
25
  end
36
26
 
@@ -41,13 +31,6 @@ module ScaffoldProjectDetector
41
31
  'selenium'
42
32
  end
43
33
 
44
- def detect_framework(gemfile = read_gemfile)
45
- FRAMEWORK_GEMS.each do |gem_name, framework|
46
- return framework if gemfile.include?("'#{gem_name}'") || gemfile.include?("\"#{gem_name}\"")
47
- end
48
- 'rspec'
49
- end
50
-
51
34
  def read_gemfile
52
35
  return '' unless File.exist?('Gemfile')
53
36
 
@@ -58,15 +41,6 @@ module ScaffoldProjectDetector
58
41
  detect_automation == 'selenium'
59
42
  end
60
43
 
61
- def watir?
62
- detect_automation == 'watir'
63
- end
64
-
65
- # Projects made before WaitHelper, or whose owner removed it, do not have the file.
66
- def wait_helper?
67
- File.exist?(WAIT_HELPER_PATH)
68
- end
69
-
70
44
  # Validates that the current directory looks like a Ruby Raider project.
71
45
  # Returns an array of warning messages (empty = valid project).
72
46
  def validate_project
@@ -21,10 +21,6 @@ class Scaffolding < Thor::Group
21
21
  template('page_object.tt', default_path("page_objects/pages/#{normalized_name}.rb", '_page.rb'))
22
22
  end
23
23
 
24
- def generate_feature
25
- template('feature.tt', default_path("features/#{normalized_name}.feature", '.feature'))
26
- end
27
-
28
24
  def generate_spec
29
25
  template('spec.tt', default_path("spec/#{normalized_name}_page_spec.rb", '_spec.rb'))
30
26
  end
@@ -33,10 +29,6 @@ class Scaffolding < Thor::Group
33
29
  template('helper.tt', default_path("helpers/#{normalized_name}_helper.rb", '_helper.rb'))
34
30
  end
35
31
 
36
- def generate_steps
37
- template('steps.tt', default_path("features/step_definitions/#{normalized_name}_steps.rb", '_steps.rb'))
38
- end
39
-
40
32
  def generate_component
41
33
  template('component.tt', default_path("page_objects/components/#{normalized_name}.rb", '.rb'))
42
34
  end
@@ -76,10 +68,6 @@ class Scaffolding < Thor::Group
76
68
  ScaffoldProjectDetector.detect_automation
77
69
  end
78
70
 
79
- def framework_type
80
- ScaffoldProjectDetector.detect_framework
81
- end
82
-
83
71
  def project_config
84
72
  ScaffoldProjectDetector.config
85
73
  end
@@ -88,14 +76,6 @@ class Scaffolding < Thor::Group
88
76
  automation_type == 'selenium'
89
77
  end
90
78
 
91
- def watir?
92
- automation_type == 'watir'
93
- end
94
-
95
- def wait_helper?
96
- ScaffoldProjectDetector.wait_helper?
97
- end
98
-
99
79
  def default_path(standard_path, file_type)
100
80
  path ? "#{path}/#{normalized_name}#{file_type}" : standard_path
101
81
  end
@@ -12,20 +12,6 @@ class <%= class_name %> < Component
12
12
  private
13
13
 
14
14
  # Elements
15
- <%- if wait_helper? -%>
16
15
  # Example: component.find_element(css: '.selector').click
17
- <%- else -%>
18
- # Example: driver.find_element(css: '.selector')
19
- <%- end -%>
20
- <%- elsif watir? -%>
21
-
22
- def content
23
- component.text
24
- end
25
-
26
- private
27
-
28
- # Elements
29
- # Example: browser.element(css: '.selector')
30
16
  <%- end -%>
31
17
  end
@@ -18,13 +18,7 @@ require_relative '../abstract/page'
18
18
 
19
19
  <%= ' ' * module_parts.size %> # Elements
20
20
  <%- if selenium? -%>
21
- <%- if wait_helper? -%>
22
- <%= ' ' * module_parts.size %> # Example: click({ id: 'element_id' })
23
- <%- else -%>
24
- <%= ' ' * module_parts.size %> # Example: driver.find_element(id: 'element_id')
25
- <%- end -%>
26
- <%- elsif watir? -%>
27
- <%= ' ' * module_parts.size %> # Example: browser.text_field(id: 'element_id')
21
+ <%= ' ' * module_parts.size %> # Example: def submit_button = driver.find_element(by_role('button', name: 'Submit'))
28
22
  <%- end -%>
29
23
  <%= ' ' * module_parts.size %>end
30
24
  <% module_parts.each_with_index do |_mod, i| -%>
@@ -44,13 +38,7 @@ class <%= page_class_name %> < Page
44
38
 
45
39
  # Elements
46
40
  <%- if selenium? -%>
47
- <%- if wait_helper? -%>
48
- # Example: click({ id: 'element_id' })
49
- <%- else -%>
50
- # Example: driver.find_element(id: 'element_id')
51
- <%- end -%>
52
- <%- elsif watir? -%>
53
- # Example: browser.text_field(id: 'element_id')
41
+ # Example: def submit_button = driver.find_element(by_role('button', name: 'Submit'))
54
42
  <%- end -%>
55
43
  end
56
44
  <% end -%>
@@ -5,18 +5,10 @@ require_relative '../page_objects/pages/<%= normalized_name %>'
5
5
  describe '<%= page_class_name %>' do
6
6
  <%- if selenium? -%>
7
7
  let(:page) { <%= page_class_name %>.new(driver) }
8
- <%- elsif watir? -%>
9
- let(:page) { <%= page_class_name %>.new(browser) }
10
8
  <%- end -%>
11
9
  <%- if project_config['url'] -%>
12
10
 
13
- before do
14
- <%- if selenium? -%>
15
- driver.navigate.to "\#{project_config['url']}/<%= normalized_name %>"
16
- <%- elsif watir? -%>
17
- browser.goto "\#{project_config['url']}/<%= normalized_name %>"
18
- <%- end -%>
19
- end
11
+ before { page.visit }
20
12
  <%- end -%>
21
13
 
22
14
  it 'loads successfully' do
@@ -23,10 +23,6 @@ module Utilities
23
23
  set('spec_path', path)
24
24
  end
25
25
 
26
- def feature_path=(path)
27
- set('feature_path', path)
28
- end
29
-
30
26
  def helper_path=(path)
31
27
  set('helper_path', path)
32
28
  end
@@ -68,6 +64,14 @@ module Utilities
68
64
  set('headless', enabled)
69
65
  end
70
66
 
67
+ def strict=(enabled)
68
+ set('strict', enabled)
69
+ end
70
+
71
+ def actionability=(enabled)
72
+ set('actionability', enabled)
73
+ end
74
+
71
75
  def video_enabled=(enabled)
72
76
  reload_config
73
77
  video = @config['video']
@@ -91,13 +95,11 @@ module Utilities
91
95
  end
92
96
 
93
97
  def run(opts = nil)
94
- command = File.directory?('spec') ? 'rspec spec/' : 'cucumber features'
95
- system "#{command} #{opts}"
98
+ system "rspec spec/ #{opts}"
96
99
  end
97
100
 
98
101
  def parallel_run(opts = nil, _settings = nil)
99
- command = File.directory?('spec') ? 'parallel_rspec spec/' : 'parallel_cucumber features'
100
- system "#{command} #{opts}"
102
+ system "parallel_rspec spec/ #{opts}"
101
103
  end
102
104
 
103
105
  private
data/lib/version CHANGED
@@ -1 +1 @@
1
- 3.13.0
1
+ 4.0.0
data/ruby_raider.gemspec CHANGED
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.bindir = 'bin'
15
15
  s.executables << 'raider'
16
16
  s.add_development_dependency 'bundler-audit', '~> 0.9'
17
+ s.add_development_dependency 'chunky_png', '~> 1.4'
17
18
  s.add_development_dependency 'rake', '~> 13.0.6'
18
19
  s.add_development_dependency 'reek', '~> 6.5'
19
20
  s.add_development_dependency 'rspec', '~> 3.11.0'
@@ -22,9 +23,9 @@ Gem::Specification.new do |s|
22
23
  s.add_development_dependency 'rubocop-rspec', '~> 2.9.0'
23
24
  s.add_development_dependency 'ruby_audit', '~> 3.1'
24
25
  s.add_development_dependency 'rubyzip', '>= 2.3', '< 4'
25
- s.add_development_dependency 'selenium-webdriver', '~> 4.0'
26
26
  s.add_development_dependency 'steep', '~> 1.9'
27
27
 
28
+ s.add_runtime_dependency 'selenium-webdriver', '>= 4.30', '< 5'
28
29
  s.add_runtime_dependency 'thor', '~> 1.2.1'
29
30
  s.add_runtime_dependency 'tty-prompt', '~> 0.23.1'
30
31
  end
@@ -1,10 +1,8 @@
1
1
  # Type definitions for ScaffoldingCommands
2
2
  class ScaffoldingCommands < Thor
3
3
  def page: (String name) -> void
4
- def feature: (String name) -> void
5
4
  def spec: (String name) -> void
6
5
  def helper: (String name) -> void
7
- def steps: (String name) -> void
8
6
  def component: (String name) -> void
9
7
  def scaffold: (*String names) -> void
10
8
 
@@ -4,6 +4,8 @@ class UtilityCommands < Thor
4
4
  def url: (String default_url) -> void
5
5
  def browser: (?String? default_browser) -> void
6
6
  def browser_options: (*String opts) -> void
7
+ def strict: (String toggle) -> void
8
+ def actionability: (String toggle) -> void
7
9
  def raid: () -> void
8
10
  def timeout: (String seconds) -> void
9
11
  def viewport: (String dimensions) -> void
@@ -16,4 +18,8 @@ class UtilityCommands < Thor
16
18
  def video_toggle: (bool enabled) -> void
17
19
  def trace: (String action) -> void
18
20
  def trace_toggle: (bool enabled) -> void
21
+
22
+ private
23
+
24
+ def on?: (String toggle) -> bool
19
25
  end