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,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
5
+
6
+ RSpec.describe 'Raider::Matchers::Base' do
7
+ after { Raider.reset }
8
+
9
+ let(:matcher_class) do
10
+ Class.new(Raider::Matchers::Base) do
11
+ def verb = 'have value'
12
+ def read(subject) = subject.value
13
+ def pass?(value) = value == expected
14
+ end
15
+ end
16
+
17
+ let(:subject_class) do
18
+ Class.new do
19
+ attr_reader :reads
20
+
21
+ def initialize(*values, error: nil)
22
+ @values = values
23
+ @error = error
24
+ @reads = 0
25
+ end
26
+
27
+ def value
28
+ @reads += 1
29
+ raise @error, 'gone' if @error
30
+
31
+ @values[[@reads, @values.size].min - 1]
32
+ end
33
+
34
+ def tag_name = raise(Selenium::WebDriver::Error::WebDriverError, 'no tag')
35
+ end
36
+ end
37
+
38
+ it 'passes once the value changes to the expected one' do
39
+ element = subject_class.new('a', 'a', 'b')
40
+ expect(matcher_class.new('b', wait: 1).matches?(element)).to be(true)
41
+ expect(element.reads).to be >= 3
42
+ end
43
+
44
+ it 'retries the negated form until the value changes away' do
45
+ element = subject_class.new('b', 'b', 'a')
46
+ expect(matcher_class.new('b', wait: 1).does_not_match?(element)).to be(true)
47
+ expect(element.reads).to be >= 3
48
+ end
49
+
50
+ it 'reports the last value seen after the wait' do
51
+ matcher = matcher_class.new('b', wait: 0.2)
52
+ expect(matcher.matches?(subject_class.new('a'))).to be(false)
53
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected <element> to have value "b", last seen "a" after ')}0\.\d+s\z/)
54
+ end
55
+
56
+ it 'words the negated failure' do
57
+ matcher = matcher_class.new('a', wait: 0.2)
58
+ expect(matcher.does_not_match?(subject_class.new('a'))).to be(false)
59
+ expect(matcher.failure_message_when_negated)
60
+ .to match(/\A#{Regexp.escape('expected <element> not to have value "a", last seen "a" after ')}0\.\d+s\z/)
61
+ end
62
+
63
+ it 'uses the configured timeout when wait is nil' do
64
+ Raider.timeout = 0.2
65
+ matcher = matcher_class.new('b')
66
+ matcher.matches?(subject_class.new('a'))
67
+ expect(matcher.failure_message).to match(/after 0\.[23]\d*s\z/)
68
+ end
69
+
70
+ it 'calls a Proc actual on every poll and describes it as the block' do
71
+ calls = 0
72
+ matcher = matcher_class.new('b', wait: 0.3)
73
+ result = matcher.matches?(proc { subject_class.new("v#{calls += 1}") })
74
+ expect(result).to be(false)
75
+ expect(calls).to be >= 2
76
+ expect(matcher.failure_message).to start_with('expected the block to have value "b", last seen "v')
77
+ end
78
+
79
+ it 'propagates an error raised by a Proc actual' do
80
+ expect { matcher_class.new('b', wait: 1).matches?(proc { raise 'boom' }) }.to raise_error(RuntimeError, 'boom')
81
+ end
82
+
83
+ it 'reports not found when the element never appears' do
84
+ element = subject_class.new(error: Selenium::WebDriver::Error::NoSuchElementError)
85
+ matcher = matcher_class.new('b', wait: 0.2)
86
+ expect(matcher.matches?(element)).to be(false)
87
+ expect(matcher.failure_message).to include('last seen: not found')
88
+ end
89
+
90
+ it 'counts absence as a pass when the matcher allows it' do
91
+ absent = Class.new(matcher_class) { def absent_passes? = true }
92
+ element = subject_class.new(error: Selenium::WebDriver::Error::StaleElementReferenceError)
93
+ expect(absent.new('x', wait: 0.2).matches?(element)).to be(true)
94
+ expect(absent.new('x', wait: 0.2).does_not_match?(element)).to be(false)
95
+ end
96
+
97
+ context 'when the read raises a selenium TimeoutError' do
98
+ before { stub_const('SlowRead', Class.new(Selenium::WebDriver::Error::TimeoutError)) }
99
+
100
+ let(:timeout_error) { SlowRead }
101
+ let(:reader_class) do
102
+ Class.new do
103
+ def initialize(error, fails)
104
+ @error = error
105
+ @fails = fails
106
+ end
107
+
108
+ def value
109
+ raise @error, 'slow' if (@fails -= 1) >= 0
110
+
111
+ 'ok'
112
+ end
113
+ end
114
+ end
115
+
116
+ it 'retries it as not yet' do
117
+ reader = reader_class.new(timeout_error, 2)
118
+ expect(matcher_class.new('ok', wait: 1).matches?(reader)).to be(true)
119
+ end
120
+
121
+ it 'counts it as absence when the matcher allows' do
122
+ absent = Class.new(matcher_class) { def absent_passes? = true }
123
+ expect(absent.new('x', wait: 1).matches?(reader_class.new(timeout_error, Float::INFINITY))).to be(true)
124
+ end
125
+
126
+ it 'still ends at the matcher deadline' do
127
+ expect(matcher_class.new('ok', wait: 0.2).matches?(reader_class.new(timeout_error, Float::INFINITY))).to be(false)
128
+ end
129
+ end
130
+
131
+ it 'reports the elapsed time rather than the configured wait' do
132
+ Raider.timeout = 30
133
+ matcher = matcher_class.new('b', wait: 0.3)
134
+ matcher.matches?(subject_class.new('a'))
135
+ seconds = matcher.failure_message[/after ([\d.]+)s/, 1].to_f
136
+ expect(seconds).to be_between(0.3, 0.5)
137
+ end
138
+
139
+ it 'fails with a message for an actual without tag_name' do
140
+ actual = Struct.new(:value).new('a')
141
+ matcher = matcher_class.new('b', wait: 0.1)
142
+ expect(matcher.matches?(actual)).to be(false)
143
+ expect(matcher.failure_message).to start_with('expected #<struct')
144
+ end
145
+
146
+ it 'supports block expectations and describes itself' do
147
+ matcher = matcher_class.new('b')
148
+ expect(matcher.supports_block_expectations?).to be(true)
149
+ expect(matcher.description).to eq('have value "b"')
150
+ end
151
+
152
+ describe '#read_with_zero_wait' do
153
+ let(:reader) { matcher_class.new('x') }
154
+
155
+ it 'passes timeout: 0 when the method accepts it' do
156
+ target = Class.new { def text(timeout: 5) = timeout }.new
157
+ expect(reader.read_with_zero_wait(target, :text)).to eq(0)
158
+ end
159
+
160
+ it 'omits it otherwise' do
161
+ target = Class.new { def text = 'plain' }.new
162
+ expect(reader.read_with_zero_wait(target, :text)).to eq('plain')
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../support/fake_bridge'
5
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
6
+
7
+ RSpec.describe 'Raider element matchers' do
8
+ include Raider::Matchers
9
+
10
+ let(:bridge) { Raider::Spec::FakeBridge.new }
11
+ let(:element) { Selenium::WebDriver::Element.new(bridge, 'e1') }
12
+ let(:gone) { Selenium::WebDriver::Error::NoSuchElementError }
13
+
14
+ after { Raider.reset }
15
+
16
+ describe 'have_text' do
17
+ it 'passes once the text arrives' do
18
+ bridge.answer(:element_text, 'Welcome', 'Welcome back Ann')
19
+ expect(have_text('Welcome back Ann', wait: 1).matches?(element)).to be(true)
20
+ end
21
+
22
+ it 'matches a whitespace-normalised substring' do
23
+ bridge.answer(:element_text, "Hi\n Welcome back Ann ")
24
+ expect(have_text('Welcome back', wait: 1).matches?(element)).to be(true)
25
+ end
26
+
27
+ it 'fails exact on extra text with the last seen value' do
28
+ bridge.answer(:element_text, 'Welcome back Ann!')
29
+ matcher = have_text('Welcome back Ann', exact: true, wait: 0.2)
30
+ expect(matcher.matches?(element)).to be(false)
31
+ expect(matcher.failure_message)
32
+ .to match(/\A#{Regexp.escape('expected <element> to have exact text "Welcome back Ann", last seen "Welcome back Ann!" after ')}0\.\d+s\z/)
33
+ end
34
+
35
+ it 'matches a regexp against the normalised text' do
36
+ bridge.answer(:element_text, "Hi Ann\n Lee")
37
+ expect(have_text(/Ann Lee/, wait: 1).matches?(element)).to be(true)
38
+ expect(have_text(/Bob/, wait: 0.2).matches?(element)).to be(false)
39
+ end
40
+
41
+ it 'words an exact regexp failure' do
42
+ bridge.answer(:element_text, 'Bob')
43
+ matcher = have_text(/Ann/, exact: true, wait: 0.2)
44
+ expect(matcher.matches?(element)).to be(false)
45
+ expect(matcher.failure_message).to start_with('expected <element> to have exact text /Ann/, last seen "Bob"')
46
+ end
47
+
48
+ it 'waits for the text to change when negated' do
49
+ bridge.answer(:element_text, 'Loading', 'Loading', 'Done')
50
+ expect(have_text('Loading', wait: 1).does_not_match?(element)).to be(true)
51
+ end
52
+
53
+ it 'words the negated failure' do
54
+ bridge.answer(:element_text, 'Loading')
55
+ matcher = have_text('Loading', wait: 0.2)
56
+ expect(matcher.does_not_match?(element)).to be(false)
57
+ expect(matcher.failure_message_when_negated)
58
+ .to match(/\A#{Regexp.escape('expected <element> not to have text "Loading", last seen "Loading" after ')}0\.\d+s\z/)
59
+ end
60
+
61
+ it 'reads a template element with timeout: 0 so the wait is not doubled' do
62
+ seen = []
63
+ reader = Object.new
64
+ reader.define_singleton_method(:text) { |timeout: 5| seen << timeout && 'hello' }
65
+ expect(have_text('hello', wait: 1).matches?(reader)).to be(true)
66
+ expect(seen).to eq([0])
67
+ end
68
+
69
+ it 'does not pass a timeout to a reader that does not take one' do
70
+ expect(have_text('hello', wait: 1).matches?(Struct.new(:text).new('hello'))).to be(true)
71
+ end
72
+ end
73
+
74
+ describe 'have_value' do
75
+ it 'reads the value attribute' do
76
+ bridge.answer(:element_attribute, nil, 'admin')
77
+ expect(have_value('admin', wait: 1).matches?(element)).to be(true)
78
+ end
79
+ end
80
+
81
+ describe 'have_value with a regexp' do
82
+ it 'matches the value' do
83
+ bridge.answer(:element_attribute, 'xyz')
84
+ expect(have_value(/y/, wait: 1).matches?(element)).to be(true)
85
+ end
86
+ end
87
+
88
+ describe 'have_attribute' do
89
+ it 'passes when the attribute is present' do
90
+ bridge.answer(:element_attribute, nil, '/home')
91
+ expect(have_attribute('href', wait: 1).matches?(element)).to be(true)
92
+ end
93
+
94
+ it 'fails when the attribute stays absent' do
95
+ bridge.answer(:element_attribute, nil)
96
+ expect(have_attribute('href', wait: 0.2).matches?(element)).to be(false)
97
+ end
98
+
99
+ it 'matches a regexp' do
100
+ bridge.answer(:element_attribute, 'btn', 'btn active')
101
+ expect(have_attribute('class', /active/, wait: 1).matches?(element)).to be(true)
102
+ end
103
+
104
+ it 'compares a string by equality' do
105
+ bridge.answer(:element_attribute, 'btn active')
106
+ expect(have_attribute('class', 'btn', wait: 0.2).matches?(element)).to be(false)
107
+ end
108
+ end
109
+
110
+ describe 'state matchers' do
111
+ it 'be_visible passes once displayed turns true' do
112
+ bridge.answer(:element_displayed?, false, true)
113
+ expect(be_visible(wait: 1).matches?(element)).to be(true)
114
+ end
115
+
116
+ it 'be_hidden passes in one poll on a block whose find is always empty' do
117
+ driver = Raider::Spec.driver(bridge)
118
+ calls = 0
119
+ Raider.timeout = 5
120
+ expect { (calls += 1) && driver.find_element(id: 'gone') }.to be_hidden
121
+ expect(calls).to eq(1)
122
+ end
123
+
124
+ it 'have_text fails on a block whose find is always empty after about three polls' do
125
+ driver = Raider::Spec.driver(bridge)
126
+ calls = 0
127
+ matcher = have_text('x', wait: 0.3)
128
+ expect(matcher.matches?(-> { (calls += 1) && driver.find_element(id: 'gone') })).to be(false)
129
+ expect(calls).to be_between(3, 4)
130
+ end
131
+
132
+ it 'be_hidden passes when the element is absent' do
133
+ bridge.answer(:element_displayed?, gone)
134
+ expect(be_hidden(wait: 0.2).matches?(element)).to be(true)
135
+ end
136
+
137
+ it 'be_hidden passes when not displayed' do
138
+ bridge.answer(:element_displayed?, false)
139
+ expect(be_hidden(wait: 0.2).matches?(element)).to be(true)
140
+ end
141
+
142
+ it 'be_hidden fails on a visible element with the last seen state' do
143
+ bridge.answer(:element_displayed?, true)
144
+ matcher = be_hidden(wait: 0.2)
145
+ expect(matcher.matches?(element)).to be(false)
146
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected <element> to be hidden, last seen visible after ')}0\.\d+s\z/)
147
+ end
148
+
149
+ it 'be_visible failure reads last seen hidden' do
150
+ bridge.answer(:element_displayed?, false)
151
+ matcher = be_visible(wait: 0.2)
152
+ expect(matcher.matches?(element)).to be(false)
153
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected <element> to be visible, last seen hidden after ')}0\.\d+s\z/)
154
+ end
155
+
156
+ it 'be_enabled and be_disabled read enabled?' do
157
+ bridge.answer(:element_enabled?, false, true)
158
+ expect(be_enabled(wait: 1).matches?(element)).to be(true)
159
+ bridge.answer(:element_enabled?, false)
160
+ expect(be_disabled(wait: 1).matches?(element)).to be(true)
161
+ end
162
+
163
+ it 'be_checked reads selected?' do
164
+ bridge.answer(:element_selected?, false, true)
165
+ expect(be_checked(wait: 1).matches?(element)).to be(true)
166
+ end
167
+
168
+ it 'be_checked failure reads last seen unchecked' do
169
+ bridge.answer(:element_selected?, false)
170
+ matcher = be_checked(wait: 0.2)
171
+ expect(matcher.matches?(element)).to be(false)
172
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected <element> to be checked, last seen unchecked after ')}0\.\d+s\z/)
173
+ end
174
+ end
175
+
176
+ describe 'absence and staleness' do
177
+ it 'not_to be_visible passes on an absent element' do
178
+ bridge.answer(:element_displayed?, gone)
179
+ expect(be_visible(wait: 1).does_not_match?(element)).to be(true)
180
+ end
181
+
182
+ it 'not_to be_visible passes on a stale raw element' do
183
+ bridge.answer(:element_displayed?, Selenium::WebDriver::Error::StaleElementReferenceError)
184
+ expect(be_visible(wait: 1).does_not_match?(element)).to be(true)
185
+ end
186
+
187
+ it 'be_visible keeps retrying an absent element and fails' do
188
+ bridge.answer(:element_displayed?, gone)
189
+ matcher = be_visible(wait: 0.2)
190
+ expect(matcher.matches?(element)).to be(false)
191
+ expect(matcher.failure_message).to include('last seen: not found')
192
+ end
193
+
194
+ it 'fails at once with the hint for a stale raw element' do
195
+ bridge.answer(:element_text, Selenium::WebDriver::Error::StaleElementReferenceError)
196
+ matcher = have_text('x', wait: 5)
197
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
198
+ expect(matcher.matches?(element)).to be(false)
199
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 1
200
+ expect(matcher.failure_message).to eq(
201
+ 'expected <element> to have text "x", but the element is stale; ' \
202
+ 'use the block form (expect { page.element }) so it is found again'
203
+ )
204
+ end
205
+
206
+ it 'be_hidden passes on a stale raw element' do
207
+ bridge.answer(:element_displayed?, Selenium::WebDriver::Error::StaleElementReferenceError)
208
+ expect(be_hidden(wait: 1).matches?(element)).to be(true)
209
+ end
210
+
211
+ it 'retries a stale element read through a block' do
212
+ bridge.answer(:element_text, Selenium::WebDriver::Error::StaleElementReferenceError, 'x')
213
+ expect(have_text('x', wait: 1).matches?(proc { element })).to be(true)
214
+ end
215
+
216
+ it 'composes with and' do
217
+ bridge.answer(:element_text, 'abc')
218
+ bridge.answer(:element_displayed?, true)
219
+ expect(element).to have_text('a', wait: 1).and be_visible(wait: 1)
220
+ end
221
+ end
222
+
223
+ describe 'compound matchers' do
224
+ before do
225
+ bridge.answer(:element_text, 'abc')
226
+ bridge.answer(:element_displayed?, true)
227
+ end
228
+
229
+ it 'passes the block form of and when both hold' do
230
+ expect { element }.to have_text('a', wait: 1).and be_visible(wait: 1)
231
+ end
232
+
233
+ it 'fails the block form of and with a combined message' do
234
+ matcher = have_text('a', wait: 0.1).and be_hidden(wait: 0.1)
235
+ expect(matcher.matches?(proc { element })).to be(false)
236
+ expect(matcher.failure_message).to include('to be hidden')
237
+ end
238
+
239
+ it 'passes the block form of or when one side holds' do
240
+ expect { element }.to have_text('zzz', wait: 0.1).or be_visible(wait: 0.1)
241
+ end
242
+
243
+ it 'fails the block form of or when neither holds' do
244
+ matcher = have_text('zzz', wait: 0.1).or be_hidden(wait: 0.1)
245
+ expect(matcher.matches?(proc { element })).to be(false)
246
+ end
247
+
248
+ it 'passes a three-way block chain when all hold' do
249
+ bridge.answer(:element_enabled?, true)
250
+ expect { element }.to have_text('a', wait: 1).and(be_visible(wait: 1)).and be_enabled(wait: 1)
251
+ end
252
+
253
+ it 'fails a three-way block chain when the last does not hold' do
254
+ bridge.answer(:element_enabled?, false)
255
+ matcher = have_text('a', wait: 0.1).and(be_visible(wait: 0.1)).and be_enabled(wait: 0.1)
256
+ expect(matcher.matches?(proc { element })).to be(false)
257
+ expect(matcher.failure_message).to include('to be enabled')
258
+ end
259
+
260
+ it 'mixes or into a block chain' do
261
+ expect { element }.to have_text('zzz', wait: 0.1).or(be_hidden(wait: 0.1)).or be_visible(wait: 0.1)
262
+ end
263
+
264
+ it 'keeps the value form working' do
265
+ expect(element).to have_text('a', wait: 1) & be_visible(wait: 1)
266
+ end
267
+
268
+ it 'keeps the value form of or working' do
269
+ expect(element).to have_text('zzz', wait: 0.1) | be_visible(wait: 0.1)
270
+ end
271
+ end
272
+
273
+ it 'works through expect syntax with a block' do
274
+ bridge.answer(:element_text, 'x')
275
+ expect { element }.to have_text('x', wait: 1)
276
+ end
277
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
5
+
6
+ RSpec.describe 'Raider page matchers' do
7
+ include Raider::Matchers
8
+
9
+ # A driver whose title and url change from poll to poll; the last answer repeats.
10
+ let(:driver_class) do
11
+ Class.new do
12
+ def initialize(titles, urls)
13
+ @titles = titles
14
+ @urls = urls
15
+ end
16
+
17
+ def title
18
+ @titles.size > 1 ? @titles.shift : @titles.first
19
+ end
20
+
21
+ def current_url
22
+ @urls.size > 1 ? @urls.shift : @urls.first
23
+ end
24
+ end
25
+ end
26
+
27
+ after { Raider.reset }
28
+
29
+ describe 'have_count' do
30
+ it 'passes at once on an Array of the right size' do
31
+ expect(have_count(2).matches?([1, 2])).to be(true)
32
+ end
33
+
34
+ it 'fails an Array without waiting' do
35
+ matcher = have_count(2, wait: 10)
36
+ started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ expect(matcher.matches?([1])).to be(false)
38
+ expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 0.05
39
+ expect(matcher.failure_message).to eq('expected the array to have count 2, last seen 1 after 0s')
40
+ end
41
+
42
+ it 'passes on the first poll for a block whose find_elements is always empty' do
43
+ driver = Raider::Spec.driver(Raider::Spec::FakeBridge.new)
44
+ calls = 0
45
+ Raider.timeout = 5
46
+ expect { (calls += 1) && driver.find_elements(css: '.row') }.to have_count(0)
47
+ expect(calls).to eq(1)
48
+ end
49
+
50
+ it 'keeps calling a block until the count arrives' do
51
+ calls = 0
52
+ expect { (calls += 1) > 2 ? [1, 2, 3] : [] }.to have_count(3)
53
+ end
54
+
55
+ it 'words a block failure with the last count' do
56
+ matcher = have_count(3, wait: 0.2)
57
+ expect(matcher.matches?(-> { [1, 2] })).to be(false)
58
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected the block to have count 3, last seen 2 after ')}0\.\d+s\z/)
59
+ end
60
+
61
+ it 'waits for the count to leave when negated' do
62
+ sizes = [[1, 2, 3], [1, 2, 3], [1]]
63
+ expect(have_count(3, wait: 1).does_not_match?(-> { sizes.size > 1 ? sizes.shift : sizes.first })).to be(true)
64
+ end
65
+ end
66
+
67
+ describe 'have_title' do
68
+ it 'passes once the title changes' do
69
+ driver = driver_class.new(%w[Loading Account], [''])
70
+ expect(have_title('Account', wait: 1).matches?(driver)).to be(true)
71
+ end
72
+
73
+ it 'accepts a Regexp' do
74
+ expect(driver_class.new(['My Account'], [''])).to have_title(/Account\z/)
75
+ end
76
+
77
+ it 'fails with the last title' do
78
+ matcher = have_title('Account', wait: 0.2)
79
+ expect(matcher.matches?(driver_class.new(['Home'], ['']))).to be(false)
80
+ expect(matcher.failure_message).to match(/\A#{Regexp.escape('expected the page to have title "Account", last seen "Home" after ')}0\.\d+s\z/)
81
+ end
82
+ end
83
+
84
+ describe 'have_url' do
85
+ let(:driver) { driver_class.new([''], ['https://x/account']) }
86
+
87
+ it 'matches a Regexp' do
88
+ expect(driver).to have_url(%r{/account$})
89
+ end
90
+
91
+ it 'matches a whole String' do
92
+ expect(driver).to have_url('https://x/account')
93
+ end
94
+
95
+ it 'does not match a fragment of the url' do
96
+ matcher = have_url('account', wait: 0.2)
97
+ expect(matcher.matches?(driver)).to be(false)
98
+ expect(matcher.failure_message)
99
+ .to match(/\A#{Regexp.escape('expected the page to have url "account", last seen "https://x/account" after ')}0\.\d+s\z/)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec'
4
+ require_relative '../../../lib/ruby_raider/runtime'
5
+ require_relative '../../../lib/ruby_raider/runtime/rspec'
6
+ require_relative '../support/fake_bridge'
7
+
8
+ RSpec.describe 'Raider presence matchers' do
9
+ include Raider::Matchers
10
+
11
+ let(:bridge) { Raider::Spec::FakeBridge.new }
12
+ let(:driver) { Raider::Spec.driver(bridge) }
13
+
14
+ before { Raider.timeout = 0.3 }
15
+
16
+ after { Raider.reset }
17
+
18
+ it 'passes on the second poll once the element appears' do
19
+ bridge.enqueue([])
20
+ bridge.enqueue(['e'])
21
+ expect(driver).to have_element(id: 'x')
22
+ expect(bridge.calls.count { |call| call.first == :find_elements_by }).to eq(2)
23
+ end
24
+
25
+ it 'passes a negated expectation on the first poll when nothing matches' do
26
+ expect(driver).not_to have_element(id: 'x')
27
+ expect(bridge.calls.count { |call| call.first == :find_elements_by }).to eq(1)
28
+ end
29
+
30
+ it 'names the locator and the last count on failure' do
31
+ matcher = have_element({ id: 'x' }, wait: 0.1)
32
+ expect(matcher.matches?(driver)).to be(false)
33
+ expect(matcher.failure_message).to match(/to have element .*id: "x".*last seen 0/)
34
+ end
35
+
36
+ it 'builds a css locator from have_css' do
37
+ expect(have_css('.row').expected).to eq({ css: '.row' })
38
+ end
39
+
40
+ it 'builds an xpath locator from have_xpath' do
41
+ expect(have_xpath('//p', wait: 1).expected).to eq({ xpath: '//p' })
42
+ end
43
+
44
+ it 'refuses a hash together with keywords' do
45
+ expect { have_element({ id: 'x' }, css: '.y') }.to raise_error(ArgumentError)
46
+ end
47
+
48
+ it 'refuses no locator at all' do
49
+ expect { have_element }.to raise_error(ArgumentError)
50
+ end
51
+
52
+ it 'says the block when the subject is a block' do
53
+ Raider.timeout = 0.1
54
+ bridge.enqueue(['e'])
55
+ bridge.enqueue(['e'])
56
+ expect { expect { driver }.not_to have_element(id: 'x') }
57
+ .to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected the block not to have element/)
58
+ end
59
+ end