ruby_raider 3.11.0 → 3.12.1

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/reek.yml +5 -1
  3. data/.github/workflows/release.yml +2 -1
  4. data/.github/workflows/system_tests.yml +1 -1
  5. data/.reek.yml +13 -0
  6. data/.ruby-version +1 -1
  7. data/README.md +43 -4
  8. data/lib/commands/utility_commands.rb +1 -1
  9. data/lib/generators/automation/automation_generator.rb +15 -10
  10. data/lib/generators/automation/templates/appium_caps.tt +2 -4
  11. data/lib/generators/automation/templates/home.tt +1 -2
  12. data/lib/generators/automation/templates/login.tt +6 -6
  13. data/lib/generators/automation/templates/page.tt +18 -6
  14. data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
  15. data/lib/generators/automation/templates/partials/initialize_selector.tt +3 -3
  16. data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
  17. data/lib/generators/automation/templates/partials/url_methods.tt +3 -2
  18. data/lib/generators/automation/templates/partials/visit_method.tt +5 -5
  19. data/lib/generators/automation/templates/pdp.tt +3 -2
  20. data/lib/generators/cucumber/cucumber_generator.rb +16 -3
  21. data/lib/generators/cucumber/templates/env.tt +3 -3
  22. data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
  23. data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
  24. data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
  25. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
  26. data/lib/generators/cucumber/templates/steps.tt +2 -2
  27. data/lib/generators/cucumber/templates/user_factory.tt +2 -0
  28. data/lib/generators/cucumber/templates/world.tt +5 -3
  29. data/lib/generators/generator.rb +29 -14
  30. data/lib/generators/helper_generator.rb +8 -0
  31. data/lib/generators/invoke_generators.rb +23 -0
  32. data/lib/generators/menu_generator.rb +26 -14
  33. data/lib/generators/rspec/rspec_generator.rb +10 -3
  34. data/lib/generators/rspec/templates/accessibility_spec.tt +10 -10
  35. data/lib/generators/rspec/templates/spec.tt +17 -2
  36. data/lib/generators/template_renderer/template_error.rb +8 -8
  37. data/lib/generators/templates/common/gemfile.tt +9 -9
  38. data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
  39. data/lib/generators/templates/common/partials/web_config.tt +1 -1
  40. data/lib/generators/templates/common/read_me.tt +508 -24
  41. data/lib/generators/templates/helpers/appium_helper.tt +17 -15
  42. data/lib/generators/templates/helpers/browser_helper.tt +1 -2
  43. data/lib/generators/templates/helpers/partials/appium_driver.tt +1 -1
  44. data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
  45. data/lib/generators/templates/helpers/raider_element.tt +159 -0
  46. data/lib/generators/templates/helpers/session_state.tt +46 -6
  47. data/lib/generators/templates/helpers/spec_helper.tt +10 -2
  48. data/lib/generators/templates/helpers/wait_helper.tt +470 -0
  49. data/lib/ruby_raider/ffmpeg.rb +7 -7
  50. data/lib/ruby_raider.rb +9 -0
  51. data/lib/scaffolding/project_detector.rb +6 -0
  52. data/lib/scaffolding/scaffolding.rb +4 -0
  53. data/lib/scaffolding/templates/component.tt +4 -0
  54. data/lib/scaffolding/templates/page_object.tt +8 -0
  55. data/lib/version +1 -1
  56. data/sig/generators/generator.rbs +4 -0
  57. data/sig/ruby_raider.rbs +1 -0
  58. data/sig/scaffolding/project_detector.rbs +1 -0
  59. data/sig/scaffolding/scaffolding.rbs +1 -0
  60. data/spec/commands/raider_commands_spec.rb +48 -0
  61. data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
  62. data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
  63. data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +37 -0
  64. data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
  65. data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
  66. data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +37 -0
  67. data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
  68. data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
  69. data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
  70. data/spec/integration/commands/utility_commands_spec.rb +8 -0
  71. data/spec/integration/content/config_content_spec.rb +44 -0
  72. data/spec/integration/content/content_helper.rb +1 -5
  73. data/spec/integration/content/helper_content_spec.rb +79 -0
  74. data/spec/integration/content/page_content_spec.rb +75 -4
  75. data/spec/integration/content/readme_content_spec.rb +126 -5
  76. data/spec/integration/content/rubocop_content_spec.rb +87 -0
  77. data/spec/integration/content/syntax_validation_spec.rb +33 -30
  78. data/spec/integration/content/test_content_spec.rb +60 -0
  79. data/spec/integration/end_to_end_spec.rb +73 -28
  80. data/spec/integration/frozen_string_literal_matcher.rb +13 -0
  81. data/spec/integration/generators/automation_generator_spec.rb +50 -0
  82. data/spec/integration/generators/common_generator_spec.rb +20 -0
  83. data/spec/integration/generators/config_features_spec.rb +2 -2
  84. data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
  85. data/spec/integration/generators/helpers_generator_spec.rb +38 -0
  86. data/spec/integration/generators/invoke_generators_spec.rb +130 -0
  87. data/spec/integration/generators/rspec_generator_spec.rb +20 -0
  88. data/spec/integration/helpers/session_state_spec.rb +46 -0
  89. data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
  90. data/spec/integration/helpers/wait_helper_spec.rb +496 -0
  91. data/spec/integration/helpers/wait_helper_support.rb +183 -0
  92. data/spec/integration/scaffolding_e2e_spec.rb +1 -5
  93. data/spec/integration/settings_helper.rb +4 -1
  94. data/spec/integration/spec_helper.rb +2 -2
  95. data/spec/menus/menu_generator_spec.rb +64 -2
  96. data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
  97. metadata +17 -2
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wait_helper_support'
4
+
5
+ # The mobile render loads into App, so its WaitHelper and RaiderElement never meet the web
6
+ # render's top-level ones when both spec files run in one process.
7
+ module WaitHelperMobileSpec
8
+ module App; end
9
+ WaitHelperSpec.load_templates(mobile: true, into: App)
10
+
11
+ class Page
12
+ include App::WaitHelper
13
+
14
+ attr_reader :driver
15
+
16
+ def initialize(driver)
17
+ @driver = driver
18
+ end
19
+ end
20
+
21
+ RSpec.describe 'WaitHelper on mobile' do
22
+ let(:driver) { WaitHelperSpec::FakeDriver.new(device: :ios) }
23
+ let(:android) { WaitHelperSpec::FakeDriver.new(device: :android) }
24
+ let(:page) { Page.new(driver) }
25
+ let(:per_os) { { ios: { accessibility_id: 'ios-card' }, android: { accessibility_id: 'android-card' } } }
26
+
27
+ def element(**state) = WaitHelperSpec::FakeElement.new(**state)
28
+ def clock = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+ def screen_area = { left: 0, top: 20, width: 390, height: 800 }
30
+
31
+ describe '#tap and #click' do
32
+ it 'click once the element is visible, enabled and stable' do
33
+ %i[tap click].each do |name|
34
+ target = element(rects: [[0, 0, 10, 10], [5, 5, 10, 10], [5, 5, 10, 10]])
35
+ driver.script({ accessibility_id: name.to_s }, nil, element(displayed: false), element(enabled: false), target)
36
+ expect(page.public_send(name, { accessibility_id: name.to_s }, timeout: 2)).to eq(target)
37
+ expect(target.log).to eq([[:click]])
38
+ end
39
+ end
40
+ end
41
+
42
+ describe 'tap with a block' do
43
+ it 'is Object#tap on a RaiderElement: yields and returns the wrapper without clicking' do
44
+ card = element
45
+ driver.script({ accessibility_id: 'card' }, card)
46
+ wrapper = page.find_element(accessibility_id: 'card')
47
+ seen = nil
48
+ expect(wrapper.tap { |yielded| seen = yielded }).to be(wrapper)
49
+ expect([seen, card.log]).to eq([wrapper, []])
50
+ end
51
+
52
+ it 'is Object#tap on the mixin: yields self without looking anything up' do
53
+ seen = nil
54
+ expect(page.tap { |yielded| seen = yielded }).to be(page)
55
+ expect(page.tap(&:driver)).to be(page)
56
+ expect([seen, driver.finds, driver.scripts]).to eq([page, [], []])
57
+ end
58
+
59
+ it 'still clicks without a block' do
60
+ card = element
61
+ driver.script({ id: 'x' }, card)
62
+ page.tap({ id: 'x' }, timeout: 1)
63
+ page.tap(id: 'x')
64
+ page.find_element(id: 'x').tap
65
+ expect(card.log).to eq([[:click]] * 3)
66
+ end
67
+ end
68
+
69
+ describe '#swipe' do
70
+ it 'uses mobile: swipe on iOS' do
71
+ card = element
72
+ driver.script({ accessibility_id: 'card' }, card)
73
+ expect(page.swipe('left', { accessibility_id: 'card' }, timeout: 1)).to eq(card)
74
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'left', element: card.id }]])
75
+ end
76
+
77
+ it 'uses mobile: swipeGesture on Android' do
78
+ card = element
79
+ android.script({ accessibility_id: 'card' }, card)
80
+ Page.new(android).swipe('left', { accessibility_id: 'card' }, timeout: 1)
81
+ expect(android.scripts)
82
+ .to eq([['mobile: swipeGesture', { elementId: card.id, direction: 'left', percent: 0.75 }]])
83
+ end
84
+
85
+ it 'waits for the element to be visible first' do
86
+ card = element
87
+ driver.script({ accessibility_id: 'card' }, nil, element(displayed: false), card)
88
+ page.swipe('up', { accessibility_id: 'card' }, timeout: 1)
89
+ expect([driver.finds.size, driver.scripts.size]).to eq([3, 1])
90
+ end
91
+
92
+ it 'swipes the whole screen without a locator' do
93
+ expect(page.swipe('up')).to be_nil
94
+ Page.new(android).swipe('up')
95
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'up' }]])
96
+ expect(android.scripts).to eq([['mobile: swipeGesture', { **screen_area, direction: 'up', percent: 0.75 }]])
97
+ end
98
+
99
+ it 'rejects a direction other than up, down, left or right' do
100
+ expect { page.swipe('sideways') }.to raise_error(ArgumentError, /up, down, left or right/)
101
+ expect(driver.scripts).to be_empty
102
+ end
103
+ end
104
+
105
+ describe '#scroll' do
106
+ it 'scrolls the screen without a locator' do
107
+ page.scroll('down')
108
+ Page.new(android).scroll('down')
109
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'down' }]])
110
+ expect(android.scripts).to eq([['mobile: scrollGesture', { **screen_area, direction: 'down', percent: 0.75 }]])
111
+ end
112
+
113
+ it 'scrolls a container' do
114
+ list = element
115
+ android.script({ id: 'list' }, list)
116
+ Page.new(android).scroll('up', { id: 'list' }, timeout: 1)
117
+ expect(android.scripts).to eq([['mobile: scrollGesture', { elementId: list.id, direction: 'up', percent: 0.75 }]])
118
+ end
119
+ end
120
+
121
+ describe 'an unknown device' do
122
+ it 'raises NotImplementedError naming it, before waiting' do
123
+ windows = WaitHelperSpec::FakeDriver.new(device: :windows)
124
+ started = clock
125
+ expect { Page.new(windows).swipe('left', { id: 'never' }, timeout: 2) }
126
+ .to raise_error(NotImplementedError, /windows/)
127
+ expect { Page.new(windows).scroll('down') }.to raise_error(NotImplementedError, /windows/)
128
+ expect(clock - started).to be < 0.5
129
+ end
130
+ end
131
+
132
+ describe '#scroll_to' do
133
+ it 'returns a RaiderElement once the element turns visible after 2 scrolls' do
134
+ footer = element
135
+ driver.script({ accessibility_id: 'footer' }, nil, element(displayed: false), footer)
136
+ found = page.scroll_to({ accessibility_id: 'footer' }, timeout: 2)
137
+ expect(found).to be_a(App::RaiderElement).and eq(footer)
138
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'down' }]] * 2)
139
+ end
140
+
141
+ it 'scrolls in the given direction' do
142
+ driver.script({ accessibility_id: 'header' }, nil, element)
143
+ page.scroll_to({ accessibility_id: 'header' }, direction: 'up', timeout: 2)
144
+ expect(driver.scripts).to eq([['mobile: scroll', { direction: 'up' }]])
145
+ end
146
+
147
+ it 'stops after max_scrolls scrolls and says so' do
148
+ started = clock
149
+ expect { page.scroll_to({ accessibility_id: 'footer' }, max_scrolls: 3, timeout: 5) }
150
+ .to raise_error(App::WaitHelper::TimeoutError) { |error|
151
+ expect(error.message).to include('to be visible').and include('after 3 scrolls')
152
+ }
153
+ expect(driver.scripts.size).to eq(3)
154
+ expect(driver.finds.size).to eq(4)
155
+ expect(clock - started).to be < 2
156
+ end
157
+
158
+ it 'also stops at the timeout, naming the scrolls so far' do
159
+ expect { page.scroll_to({ accessibility_id: 'footer' }, timeout: 0) }
160
+ .to raise_error(App::WaitHelper::TimeoutError, /after 0 scrolls/)
161
+ expect(driver.scripts).to be_empty
162
+ end
163
+ end
164
+
165
+ describe 'per-OS locators' do
166
+ it 'resolve on tap and swipe' do
167
+ card = element
168
+ android.script({ accessibility_id: 'android-card' }, card)
169
+ mobile = Page.new(android)
170
+ mobile.tap(per_os, timeout: 1)
171
+ mobile.swipe('right', per_os, timeout: 1)
172
+ expect(android.finds.uniq).to eq([{ accessibility_id: 'android-card' }])
173
+ expect([card.log, android.scripts.size]).to eq([[[:click]], 1])
174
+ end
175
+
176
+ it 'resolve on a nested find_element' do
177
+ row = element
178
+ list = element.script({ accessibility_id: 'ios-row' }, row)
179
+ driver.script({ accessibility_id: 'ios-card' }, list)
180
+ found = page.find_element(per_os).find_element({ ios: { accessibility_id: 'ios-row' }, android: { id: 'row' } })
181
+ expect(found).to eq(row)
182
+ end
183
+ end
184
+
185
+ describe 'RaiderElement' do
186
+ it 'has the mobile set without the locator' do
187
+ card = element
188
+ driver.script({ accessibility_id: 'card' }, card)
189
+ wrapper = page.find_element(accessibility_id: 'card')
190
+ expect(wrapper.tap.swipe('left').scroll('down').scroll_to).to be(wrapper)
191
+ expect(card.log).to eq([[:click]])
192
+ expect(driver.scripts).to eq([['mobile: swipe', { direction: 'left', element: card.id }],
193
+ ['mobile: scroll', { direction: 'down', element: card.id }]])
194
+ end
195
+
196
+ it 'adds exactly the planned public methods' do
197
+ expect(App::RaiderElement.public_instance_methods(false).sort)
198
+ .to eq(%i[== attribute clear click displayed? enabled? eql? find_element find_elements hash native scroll
199
+ scroll_to selected? send_keys swipe tap text])
200
+ end
201
+ end
202
+
203
+ describe 'the choke point' do
204
+ let(:actions) { [] }
205
+
206
+ before do
207
+ driver.script({ id: 'x' }, element.script({ css: 'y' }, element).script_many({ css: 'y' }, [element]))
208
+ allow_any_instance_of(App::WaitHelper::Waiter).to receive(:perform_action).and_wrap_original do |original, *args, **options, &block|
209
+ actions << args.first
210
+ original.call(*args, **options, &block)
211
+ end
212
+ end
213
+
214
+ def calls(locator)
215
+ { find_element: [locator], find_elements: [locator], click: [locator], tap: [locator], clear: [locator],
216
+ text: [locator], displayed?: [locator], enabled?: [locator], selected?: [locator],
217
+ send_keys: [locator, 'a'], attribute: [locator, 'name'], swipe: ['left', locator],
218
+ scroll: ['down', locator], scroll_to: [locator] }
219
+ end
220
+
221
+ it 'is called once per mixin method, with the method name' do
222
+ calls({ id: 'x' }).each do |name, args|
223
+ actions.clear
224
+ page.public_send(name, *args, timeout: 1)
225
+ expect(actions).to eq([name])
226
+ end
227
+ end
228
+
229
+ it 'is called once for a whole-screen gesture' do
230
+ %i[swipe scroll].each do |name|
231
+ actions.clear
232
+ page.public_send(name, 'up')
233
+ expect(actions).to eq([name])
234
+ end
235
+ end
236
+
237
+ it 'is called once per wrapper method, with the method name' do
238
+ wrapper = page.find_element(id: 'x')
239
+ calls({ css: 'y' }).each do |name, args|
240
+ args = args.reject { |arg| arg.is_a?(Hash) } unless %i[find_element find_elements].include?(name)
241
+ actions.clear
242
+ wrapper.public_send(name, *args, timeout: 1)
243
+ expect(actions).to eq([name])
244
+ end
245
+ end
246
+ end
247
+
248
+ describe 'footprint' do
249
+ it 'adds exactly the mobile API, with no private methods and nothing named wait' do
250
+ expect(App::WaitHelper.public_instance_methods(false).sort)
251
+ .to eq(%i[attribute clear click displayed? enabled? eventually find_element find_elements scroll scroll_to
252
+ selected? send_keys swipe tap text])
253
+ expect(App::WaitHelper.private_instance_methods(false)).to be_empty
254
+ expect(page.respond_to?(:wait, true)).to be(false)
255
+ end
256
+ end
257
+
258
+ describe '#eventually' do
259
+ it 'retries the block as on web' do
260
+ tries = 0
261
+ expect(page.eventually(timeout: 1) { expect(tries += 1).to eq(2) }).to be(true)
262
+ end
263
+
264
+ it 'retries a nested timeout: until its own deadline, as on web' do
265
+ started = clock
266
+ expect { page.eventually(timeout: 0.6) { page.text({ accessibility_id: 'never' }, timeout: 0.2) } }
267
+ .to raise_error(App::WaitHelper::TimeoutError, /\A\(still failing after 0.6s\)/)
268
+ expect(clock - started).to be_between(0.5, 1.0)
269
+ end
270
+
271
+ it 'raises at once for a stale find_elements member used as a parent' do
272
+ row = element.script({ accessibility_id: 'cell' }, element)
273
+ driver.script_many({ accessibility_id: 'row' }, [row])
274
+ member = page.find_elements(accessibility_id: 'row').first
275
+ row.go_stale
276
+ expect { member.find_element(accessibility_id: 'cell', timeout: 2) }
277
+ .to raise_error(Selenium::WebDriver::Error::StaleElementReferenceError, /find_elements again/)
278
+ end
279
+ end
280
+ end
281
+ end