ruby_raider 3.11.0 → 3.12.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.
- checksums.yaml +4 -4
- data/.github/workflows/reek.yml +5 -1
- data/.github/workflows/release.yml +2 -1
- data/.github/workflows/system_tests.yml +1 -1
- data/.reek.yml +13 -0
- data/README.md +43 -4
- data/lib/commands/utility_commands.rb +1 -1
- data/lib/generators/automation/automation_generator.rb +15 -10
- data/lib/generators/automation/templates/appium_caps.tt +2 -4
- data/lib/generators/automation/templates/home.tt +1 -2
- data/lib/generators/automation/templates/login.tt +6 -6
- data/lib/generators/automation/templates/page.tt +10 -3
- data/lib/generators/automation/templates/partials/home_page_selector.tt +4 -6
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +4 -6
- data/lib/generators/automation/templates/pdp.tt +3 -2
- data/lib/generators/cucumber/cucumber_generator.rb +15 -2
- data/lib/generators/cucumber/templates/partials/driver_world.tt +9 -0
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +1 -1
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +4 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -0
- data/lib/generators/generator.rb +15 -4
- data/lib/generators/helper_generator.rb +8 -0
- data/lib/generators/invoke_generators.rb +23 -0
- data/lib/generators/menu_generator.rb +26 -14
- data/lib/generators/rspec/rspec_generator.rb +10 -3
- data/lib/generators/rspec/templates/spec.tt +15 -1
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
- data/lib/generators/templates/common/partials/web_config.tt +1 -1
- data/lib/generators/templates/common/read_me.tt +508 -24
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +1 -3
- data/lib/generators/templates/helpers/raider_element.tt +159 -0
- data/lib/generators/templates/helpers/session_state.tt +45 -5
- data/lib/generators/templates/helpers/spec_helper.tt +9 -1
- data/lib/generators/templates/helpers/wait_helper.tt +470 -0
- data/lib/ruby_raider/ffmpeg.rb +7 -7
- data/lib/ruby_raider.rb +2 -0
- data/lib/scaffolding/project_detector.rb +6 -0
- data/lib/scaffolding/scaffolding.rb +4 -0
- data/lib/scaffolding/templates/component.tt +4 -0
- data/lib/scaffolding/templates/page_object.tt +8 -0
- data/lib/version +1 -1
- data/sig/generators/generator.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +1 -0
- data/sig/scaffolding/scaffolding.rbs +1 -0
- data/spec/commands/raider_commands_spec.rb +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +175 -0
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +24 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +174 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +39 -0
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +33 -0
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +49 -0
- data/spec/integration/commands/scaffolding_commands_spec.rb +49 -0
- data/spec/integration/commands/utility_commands_spec.rb +8 -0
- data/spec/integration/content/config_content_spec.rb +44 -0
- data/spec/integration/content/helper_content_spec.rb +79 -0
- data/spec/integration/content/page_content_spec.rb +75 -4
- data/spec/integration/content/readme_content_spec.rb +126 -5
- data/spec/integration/content/syntax_validation_spec.rb +23 -0
- data/spec/integration/content/test_content_spec.rb +57 -0
- data/spec/integration/end_to_end_spec.rb +50 -28
- data/spec/integration/generators/automation_generator_spec.rb +50 -0
- data/spec/integration/generators/common_generator_spec.rb +20 -0
- data/spec/integration/generators/config_features_spec.rb +2 -2
- data/spec/integration/generators/cucumber_generator_spec.rb +36 -0
- data/spec/integration/generators/helpers_generator_spec.rb +38 -0
- data/spec/integration/generators/invoke_generators_spec.rb +70 -0
- data/spec/integration/generators/rspec_generator_spec.rb +20 -0
- data/spec/integration/helpers/session_state_spec.rb +46 -0
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +281 -0
- data/spec/integration/helpers/wait_helper_spec.rb +496 -0
- data/spec/integration/helpers/wait_helper_support.rb +183 -0
- data/spec/integration/settings_helper.rb +4 -1
- data/spec/integration/spec_helper.rb +2 -2
- data/spec/menus/menu_generator_spec.rb +64 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +12 -0
- metadata +15 -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
|
|
@@ -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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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']
|