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,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ <%- if mobile? -%>
4
+ # An element found through WaitHelper. It has the same waiting methods as the
5
+ # mixin, minus the locator, so calls chain:
6
+ # find_element(accessibility_id: 'list').find_element(accessibility_id: 'row').tap
7
+ # find_element(accessibility_id: 'card').swipe('left')
8
+ # Everything else (tag_name, rect, location, ...) goes to the raw Appium
9
+ # element without waiting. APIs that type-check their argument
10
+ # (driver.action, execute_script) need .native.
11
+ <%- else -%>
12
+ # An element found through WaitHelper. It has the same waiting methods as the
13
+ # mixin, minus the locator, so calls chain:
14
+ # find_element(css: '.form').find_element(id: 'email').send_keys('a')
15
+ # Everything else (tag_name, rect, property, ...) goes to the raw Selenium
16
+ # element without waiting. Selenium APIs that type-check their argument
17
+ # (switch_to.frame, Support::Select, driver.action, execute_script) need .native.
18
+ <%- end -%>
19
+ class RaiderElement
20
+ def initialize(driver, lookup)
21
+ @driver = driver
22
+ @lookup = lookup
23
+ end
24
+
25
+ def native
26
+ @lookup.element
27
+ end
28
+
29
+ def find_element(locator = nil, visible: true, timeout: nil, **by)
30
+ waiter(timeout).find_element(child(locator, by), visible)
31
+ end
32
+
33
+ def find_elements(locator = nil, timeout: nil, **by)
34
+ waiter(timeout).find_elements(child(locator, by))
35
+ end
36
+
37
+ <%- if mobile? -%>
38
+ %i[click clear].each do |action|
39
+ define_method(action) do |timeout: nil|
40
+ perform(action, timeout) { |element| element.public_send(action) }
41
+ self
42
+ end
43
+ end
44
+
45
+ # tap is Appium's word for click. With a block it stays Ruby's Object#tap.
46
+ def tap(timeout: nil, &block)
47
+ return super(&block) if block
48
+
49
+ perform(:tap, timeout, &:click)
50
+ self
51
+ end
52
+ <%- else -%>
53
+ %i[click clear submit].each do |action|
54
+ define_method(action) do |timeout: nil|
55
+ perform(action, timeout) { |element| element.public_send(action) }
56
+ self
57
+ end
58
+ end
59
+ <%- end -%>
60
+
61
+ %i[text displayed? enabled? selected?].each do |reader|
62
+ define_method(reader) do |timeout: nil|
63
+ perform(reader, timeout) { |element| element.public_send(reader) }
64
+ end
65
+ end
66
+
67
+ def send_keys(*keys, timeout: nil)
68
+ perform(:send_keys, timeout) { |element| element.send_keys(*keys) }
69
+ self
70
+ end
71
+
72
+ <%- unless mobile? -%>
73
+ def select_by(how, what, timeout: nil)
74
+ perform(:select_by, timeout) { |element| Selenium::WebDriver::Support::Select.new(element).select_by(how, what) }
75
+ self
76
+ end
77
+
78
+ <%- end -%>
79
+ def attribute(name, timeout: nil)
80
+ perform(:attribute, timeout) { |element| element.attribute(name) }
81
+ end
82
+
83
+ <%- if mobile? -%>
84
+ # direction: 'up', 'down', 'left' or 'right'.
85
+ %i[swipe scroll].each do |kind|
86
+ define_method(kind) do |direction, timeout: nil|
87
+ gesture = WaitHelper::Gesture.new(@driver, kind, direction)
88
+ perform(kind, timeout) { |element| gesture.perform(element) }
89
+ self
90
+ end
91
+ end
92
+
93
+ # Scrolls the screen until this element is visible, up to max_scrolls times.
94
+ def scroll_to(direction: 'down', max_scrolls: 10, timeout: nil)
95
+ scroller = WaitHelper::Scroller.new(WaitHelper::Gesture.new(@driver, :scroll, direction), max_scrolls)
96
+ waiter(timeout).perform_action(:scroll_to, @lookup, WaitHelper::Waiter::CHECKS[:scroll_to], between: scroller) { nil }
97
+ self
98
+ end
99
+ <%- else -%>
100
+ def hover(timeout: nil)
101
+ perform(:hover, timeout) { |element| @driver.action.move_to(element).perform }
102
+ self
103
+ end
104
+
105
+ def scroll_into_view(timeout: nil)
106
+ perform(:scroll_into_view, timeout) { |element| @driver.action.scroll_to(element).perform }
107
+ self
108
+ end
109
+
110
+ # target: a RaiderElement, a raw element, or a driver-scoped locator.
111
+ def drag_and_drop(target, timeout: nil)
112
+ drop = WaitHelper::Lookup.for(@driver, target, {})
113
+ waiter(timeout).perform_action(:drag_and_drop, @lookup, WaitHelper::Waiter::CHECKS[:drag_and_drop],
114
+ target: drop) do |source, destination|
115
+ @driver.action.drag_and_drop(source, destination).perform
116
+ end
117
+ self
118
+ end
119
+ <%- end -%>
120
+
121
+ def ==(other)
122
+ native == (other.is_a?(RaiderElement) ? other.native : other)
123
+ end
124
+
125
+ def eql?(other)
126
+ self == other
127
+ end
128
+
129
+ def hash
130
+ native.hash
131
+ end
132
+
133
+ private
134
+
135
+ # WaitHelper::Lookup.for reads this to reuse how a wrapper was found.
136
+ attr_reader :lookup
137
+
138
+ def waiter(timeout)
139
+ WaitHelper::Waiter.new(@driver, timeout: timeout)
140
+ end
141
+
142
+ def child(locator, by)
143
+ WaitHelper::Lookup.for(@driver, locator, by, scope: @lookup)
144
+ end
145
+
146
+ def perform(action, timeout, &block)
147
+ waiter(timeout).perform_action(action, @lookup, WaitHelper::Waiter::CHECKS.fetch(action), &block)
148
+ end
149
+
150
+ def method_missing(name, *args, **options, &block)
151
+ return super unless native.respond_to?(name)
152
+
153
+ native.public_send(name, *args, **options, &block)
154
+ end
155
+
156
+ def respond_to_missing?(name, include_private = false)
157
+ native.respond_to?(name) || super
158
+ end
159
+ end
@@ -17,7 +17,7 @@ require 'yaml'
17
17
  class SessionState
18
18
  FORMAT = 1
19
19
  DEFAULT_MAX_AGE = 3600
20
- NAME_PATTERN = /\A[a-z0-9_-]+\z/.freeze
20
+ NAME_PATTERN = Regexp.new('\A[a-z0-9_-]+\z').freeze
21
21
  DIR = File.join('.raider', 'auth')
22
22
  READ_STORAGE = 'return Object.assign({}, window[arguments[0]]);'
23
23
  WRITE_STORAGE = 'var s = window[arguments[0]]; s.clear(); var d = arguments[1]; ' \
@@ -74,10 +74,17 @@ class SessionState
74
74
 
75
75
  private
76
76
 
77
+ # Warnings wait until the outcome is known: a peer's save can still rescue a failed restore,
78
+ # and each fallback warns once, only when it really logs in.
77
79
  def reuse_locked(lock, valid, &login)
78
- lock.flock(File::LOCK_EX)
79
- return if ENV.fetch('RAIDER_LOGIN_STATE', nil) != 'refresh' && fresh? && restore && still_valid?(valid)
80
-
80
+ @reason = @note = nil
81
+ @deferred = true
82
+ reused = reused?(lock, valid)
83
+ @deferred = false
84
+ return warn(@note) if reused && @note
85
+ return if reused
86
+
87
+ warn("[login_state] #{@reason}, logging in again") if @reason
81
88
  clear
82
89
  reset if @applied
83
90
  login.call
@@ -86,6 +93,35 @@ class SessionState
86
93
  lock.close
87
94
  end
88
95
 
96
+ # Restores under a shared lock, so parallel workers restore at the same time; only a real
97
+ # login takes the exclusive lock. Upgrading drops the shared lock first, so a worker that
98
+ # gets the exclusive lock after another one saved restores that login instead.
99
+ def reused?(lock, valid)
100
+ if ENV.fetch('RAIDER_LOGIN_STATE', nil) == 'refresh'
101
+ lock.flock(File::LOCK_EX)
102
+ return false
103
+ end
104
+ lock.flock(File::LOCK_SH)
105
+ seen = saved_signature
106
+ return true if restored?(valid)
107
+
108
+ lock.flock(File::LOCK_UN) # Windows will not upgrade a held shared lock in place
109
+ lock.flock(File::LOCK_EX)
110
+ saved_signature != seen && restored?(valid)
111
+ end
112
+
113
+ def restored?(valid)
114
+ fresh? && restore && still_valid?(valid)
115
+ end
116
+
117
+ # inode, mtime and size: mtime alone misses a save in the same second on coarse filesystems
118
+ def saved_signature
119
+ stat = File.stat(path)
120
+ [stat.ino, stat.mtime, stat.size]
121
+ rescue SystemCallError
122
+ nil
123
+ end
124
+
89
125
  def open_lock
90
126
  ensure_dir
91
127
  File.open(File.join(DIR, "#{name}.lock"), File::RDWR | File::CREAT, 0o600)
@@ -123,7 +159,7 @@ class SessionState
123
159
  @driver.manage.delete_all_cookies
124
160
  live, skipped = state['cookies'].partition { |cookie| live_for_origin?(cookie) }
125
161
  live.each { |cookie| add_cookie(load_cookie(cookie)) }
126
- warn("[login_state] #{skipped.size} cookies skipped (expired or another domain) from #{path}") if skipped.any?
162
+ note("[login_state] #{skipped.size} cookies skipped (expired or another domain) from #{path}") if skipped.any?
127
163
  STORAGES.each { |key, kind| @driver.execute_script(WRITE_STORAGE, kind, state[key] || {}) }
128
164
  @driver.navigate.refresh
129
165
  true
@@ -200,7 +236,11 @@ class SessionState
200
236
  end
201
237
 
202
238
  def fall_back(reason)
203
- warn("[login_state] #{reason}, logging in again")
239
+ @deferred ? (@reason ||= reason) : warn("[login_state] #{reason}, logging in again")
204
240
  false
205
241
  end
242
+
243
+ def note(message)
244
+ @deferred ? @note = message : warn(message)
245
+ end
206
246
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- <%- if axe_addon? %>
3
+ <%- if axe_addon? -%>
4
4
  require 'axe-rspec'
5
5
  <%- end -%>
6
6
  require 'rspec'
@@ -13,8 +13,13 @@ require_relative 'browser_helper'
13
13
  require_relative 'driver_helper'
14
14
  <%- end -%>
15
15
  require_relative 'video_helper'
16
+ <%- if wait_helper? -%>
17
+ require_relative 'wait_helper'
18
+ <%- end -%>
16
19
  <%- if web? -%>
17
20
  require_relative 'session_state'
21
+ <%- end -%>
22
+ <%- if web? && examples? -%>
18
23
  require_relative '../models/model_factory'
19
24
  require_relative '../page_objects/pages/login'
20
25
  require_relative '../page_objects/pages/account'
@@ -32,6 +37,9 @@ module SpecHelper
32
37
  <%- else -%>
33
38
  config.include(DriverHelper)
34
39
  <%- end -%>
40
+ <%- if wait_helper? -%>
41
+ config.include(WaitHelper)
42
+ <%- end -%>
35
43
  <%- if mobile? -%>
36
44
  config.before(:each) do
37
45
  driver.start_driver
@@ -55,7 +63,7 @@ module SpecHelper
55
63
  @video_recorder = VideoHelper.recorder_for(<%= watir? ? 'browser' : 'driver' %>)
56
64
  @video_recorder.start
57
65
  end
58
- <%- if web? -%>
66
+ <%- if web? && examples? -%>
59
67
 
60
68
  config.before(:each, :logged_in) do
61
69
  user = ModelFactory.for('users')['registered user']