ruby_raider 3.10.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 +44 -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/feature.tt +5 -0
- 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 +20 -0
- data/lib/generators/cucumber/templates/partials/watir_env.tt +16 -0
- data/lib/generators/cucumber/templates/partials/web_steps.tt +8 -0
- data/lib/generators/generator.rb +15 -4
- data/lib/generators/helper_generator.rb +15 -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 +27 -1
- data/lib/generators/template_renderer/template_error.rb +8 -8
- data/lib/generators/templates/common/git_ignore.tt +4 -1
- data/lib/generators/templates/common/partials/mobile_config.tt +1 -0
- data/lib/generators/templates/common/partials/web_config.tt +5 -2
- data/lib/generators/templates/common/read_me.tt +508 -0
- 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 +246 -0
- data/lib/generators/templates/helpers/spec_helper.tt +28 -0
- 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/ruby_raider.gemspec +1 -0
- 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/azure_content_spec.rb +4 -0
- data/spec/integration/content/ci_content_spec.rb +11 -0
- data/spec/integration/content/common_content_spec.rb +27 -0
- data/spec/integration/content/config_content_spec.rb +62 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +25 -0
- data/spec/integration/content/helper_content_spec.rb +111 -0
- data/spec/integration/content/page_content_spec.rb +75 -4
- data/spec/integration/content/readme_content_spec.rb +149 -0
- data/spec/integration/content/syntax_validation_spec.rb +38 -0
- data/spec/integration/content/test_content_spec.rb +82 -0
- data/spec/integration/end_to_end_spec.rb +73 -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 +60 -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 +474 -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 +31 -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
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'uri'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
|
|
9
|
+
# Saves the browser login (cookies, localStorage, sessionStorage) to .raider/auth/<name>.json
|
|
10
|
+
# and restores it into a new browser. The file holds live session tokens: it is git-ignored,
|
|
11
|
+
# written 0600, and its values are never printed.
|
|
12
|
+
#
|
|
13
|
+
# SessionState.new(driver, name: 'user').reuse(valid: -> { account.logged_in? }) { log_in }
|
|
14
|
+
#
|
|
15
|
+
# RAIDER_LOGIN_STATE=off always logs in and never saves; =refresh logs in and saves again.
|
|
16
|
+
# Stale age is login_state.max_age (seconds) in config/config.yml.
|
|
17
|
+
class SessionState
|
|
18
|
+
FORMAT = 1
|
|
19
|
+
DEFAULT_MAX_AGE = 3600
|
|
20
|
+
NAME_PATTERN = /\A[a-z0-9_-]+\z/.freeze
|
|
21
|
+
DIR = File.join('.raider', 'auth')
|
|
22
|
+
READ_STORAGE = 'return Object.assign({}, window[arguments[0]]);'
|
|
23
|
+
WRITE_STORAGE = 'var s = window[arguments[0]]; s.clear(); var d = arguments[1]; ' \
|
|
24
|
+
'Object.keys(d).forEach(function (k) { s.setItem(k, d[k]); });'
|
|
25
|
+
STORAGES = { 'local_storage' => 'localStorage', 'session_storage' => 'sessionStorage' }.freeze
|
|
26
|
+
COOKIE_KEYS = %i[name value domain path secure http_only same_site].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :name
|
|
29
|
+
|
|
30
|
+
def initialize(driver, name: 'user')
|
|
31
|
+
raise ArgumentError, "name must match #{NAME_PATTERN.source}" unless name.to_s.match?(NAME_PATTERN)
|
|
32
|
+
|
|
33
|
+
@driver = driver
|
|
34
|
+
@name = name.to_s
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path
|
|
38
|
+
File.join(DIR, "#{name}.json")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def reuse(valid: nil, &login)
|
|
42
|
+
lock = open_lock unless ENV.fetch('RAIDER_LOGIN_STATE', nil) == 'off'
|
|
43
|
+
lock ? reuse_locked(lock, valid, &login) : login.call
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save
|
|
48
|
+
ensure_dir
|
|
49
|
+
visit_origin
|
|
50
|
+
state = { 'format' => FORMAT, 'name' => name, 'origin' => origin, 'saved_at' => Time.now.utc.iso8601,
|
|
51
|
+
'cookies' => @driver.manage.all_cookies.map { |cookie| dump_cookie(cookie) } }
|
|
52
|
+
STORAGES.each { |key, kind| state[key] = @driver.execute_script(READ_STORAGE, kind) || {} }
|
|
53
|
+
write_atomically(JSON.generate(state))
|
|
54
|
+
path
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def restore
|
|
58
|
+
state = read_state
|
|
59
|
+
usable?(state) && apply(state)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
fall_back("could not restore #{path} (#{e.class})")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear
|
|
65
|
+
FileUtils.rm_f(path)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fresh?
|
|
69
|
+
state = read_state
|
|
70
|
+
usable?(state) && Time.now - Time.iso8601(state['saved_at']) < max_age
|
|
71
|
+
rescue ArgumentError, TypeError
|
|
72
|
+
false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
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.
|
|
79
|
+
def reuse_locked(lock, valid, &login)
|
|
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
|
|
88
|
+
clear
|
|
89
|
+
reset if @applied
|
|
90
|
+
login.call
|
|
91
|
+
save_after_login
|
|
92
|
+
ensure
|
|
93
|
+
lock.close
|
|
94
|
+
end
|
|
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
|
+
|
|
125
|
+
def open_lock
|
|
126
|
+
ensure_dir
|
|
127
|
+
File.open(File.join(DIR, "#{name}.lock"), File::RDWR | File::CREAT, 0o600)
|
|
128
|
+
rescue SystemCallError => e
|
|
129
|
+
fall_back("cannot write #{DIR} (#{e.class}), so nothing is saved")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def save_after_login
|
|
133
|
+
save
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
warn("[login_state] could not save #{path} (#{e.class}), the next run logs in again")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def reset
|
|
139
|
+
visit_origin
|
|
140
|
+
@driver.manage.delete_all_cookies
|
|
141
|
+
STORAGES.each_value { |kind| @driver.execute_script(WRITE_STORAGE, kind, {}) }
|
|
142
|
+
rescue StandardError
|
|
143
|
+
nil # a browser that cannot be reset fails the real login on its own
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def visit_origin
|
|
147
|
+
@driver.navigate.to("#{origin}/") unless origin_of(URI.parse(@driver.current_url.to_s)) == origin
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def still_valid?(valid)
|
|
151
|
+
valid.nil? || valid.call || fall_back("login restored from #{path} is no longer valid")
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
fall_back("validity check failed (#{e.class})")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def apply(state)
|
|
157
|
+
@driver.navigate.to("#{origin}/")
|
|
158
|
+
@applied = true
|
|
159
|
+
@driver.manage.delete_all_cookies
|
|
160
|
+
live, skipped = state['cookies'].partition { |cookie| live_for_origin?(cookie) }
|
|
161
|
+
live.each { |cookie| add_cookie(load_cookie(cookie)) }
|
|
162
|
+
note("[login_state] #{skipped.size} cookies skipped (expired or another domain) from #{path}") if skipped.any?
|
|
163
|
+
STORAGES.each { |key, kind| @driver.execute_script(WRITE_STORAGE, kind, state[key] || {}) }
|
|
164
|
+
@driver.navigate.refresh
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def add_cookie(attributes)
|
|
169
|
+
@driver.manage.add_cookie(attributes.dup) # Selenium's Manager rewrites the hash it is given
|
|
170
|
+
rescue StandardError
|
|
171
|
+
raise unless attributes.key?(:same_site)
|
|
172
|
+
|
|
173
|
+
@driver.manage.add_cookie(attributes.except(:same_site))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def live_for_origin?(cookie)
|
|
177
|
+
(cookie['expires'].nil? || Time.iso8601(cookie['expires']) > Time.now) && domain_matches?(cookie['domain'])
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def domain_matches?(domain)
|
|
181
|
+
bare = domain.to_s.delete_prefix('.')
|
|
182
|
+
bare.empty? || base_uri.host == bare || base_uri.host.end_with?(".#{bare}")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def dump_cookie(cookie)
|
|
186
|
+
COOKIE_KEYS.to_h { |key| [key.to_s, cookie[key]] }.merge('expires' => cookie[:expires]&.to_time&.utc&.iso8601)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def load_cookie(cookie)
|
|
190
|
+
COOKIE_KEYS.to_h { |key| [key, cookie[key.to_s]] }
|
|
191
|
+
.merge(expires: cookie['expires'] && Time.iso8601(cookie['expires'])).compact
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def read_state
|
|
195
|
+
JSON.parse(File.read(path)) if File.exist?(path)
|
|
196
|
+
rescue JSON::ParserError, SystemCallError
|
|
197
|
+
fall_back("#{path} is unreadable or corrupt")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def usable?(state)
|
|
201
|
+
return false unless state
|
|
202
|
+
return state['origin'] == origin if state.is_a?(Hash) && state['format'] == FORMAT
|
|
203
|
+
|
|
204
|
+
fall_back("#{path} has an unsupported format")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def write_atomically(json)
|
|
208
|
+
temp = "#{path}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
209
|
+
File.write(temp, json, perm: 0o600)
|
|
210
|
+
File.rename(temp, path)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def ensure_dir
|
|
214
|
+
FileUtils.chmod(0o700, FileUtils.mkdir_p(DIR))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def config
|
|
218
|
+
@config ||= YAML.load_file('config/config.yml') || {}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def max_age
|
|
222
|
+
Integer(config.dig('login_state', 'max_age') || DEFAULT_MAX_AGE)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def base_uri
|
|
226
|
+
@base_uri ||= URI.parse(config.fetch('url'))
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def origin
|
|
230
|
+
@origin ||= origin_of(base_uri)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def origin_of(uri)
|
|
234
|
+
port = uri.port == uri.default_port ? '' : ":#{uri.port}"
|
|
235
|
+
"#{uri.scheme}://#{uri.host}#{port}"
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def fall_back(reason)
|
|
239
|
+
@deferred ? (@reason ||= reason) : warn("[login_state] #{reason}, logging in again")
|
|
240
|
+
false
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def note(message)
|
|
244
|
+
@deferred ? @note = message : warn(message)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -13,6 +13,17 @@ 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 -%>
|
|
19
|
+
<%- if web? -%>
|
|
20
|
+
require_relative 'session_state'
|
|
21
|
+
<%- end -%>
|
|
22
|
+
<%- if web? && examples? -%>
|
|
23
|
+
require_relative '../models/model_factory'
|
|
24
|
+
require_relative '../page_objects/pages/login'
|
|
25
|
+
require_relative '../page_objects/pages/account'
|
|
26
|
+
<%- end -%>
|
|
16
27
|
|
|
17
28
|
module SpecHelper
|
|
18
29
|
RSpec.configure do |config|
|
|
@@ -26,6 +37,9 @@ module SpecHelper
|
|
|
26
37
|
<%- else -%>
|
|
27
38
|
config.include(DriverHelper)
|
|
28
39
|
<%- end -%>
|
|
40
|
+
<%- if wait_helper? -%>
|
|
41
|
+
config.include(WaitHelper)
|
|
42
|
+
<%- end -%>
|
|
29
43
|
<%- if mobile? -%>
|
|
30
44
|
config.before(:each) do
|
|
31
45
|
driver.start_driver
|
|
@@ -49,6 +63,20 @@ module SpecHelper
|
|
|
49
63
|
@video_recorder = VideoHelper.recorder_for(<%= watir? ? 'browser' : 'driver' %>)
|
|
50
64
|
@video_recorder.start
|
|
51
65
|
end
|
|
66
|
+
<%- if web? && examples? -%>
|
|
67
|
+
|
|
68
|
+
config.before(:each, :logged_in) do
|
|
69
|
+
user = ModelFactory.for('users')['registered user']
|
|
70
|
+
account = Account.new(<%= watir? ? 'browser' : 'driver' %>)
|
|
71
|
+
SessionState.new(<%= watir? ? 'browser.driver' : 'driver' %>).reuse(
|
|
72
|
+
valid: -> { account.tap(&:visit).header.customer_name == "Welcome back #{user['name']}" }
|
|
73
|
+
) do
|
|
74
|
+
login = Login.new(<%= watir? ? 'browser' : 'driver' %>)
|
|
75
|
+
login.visit
|
|
76
|
+
login.login(user['username'], user['password'])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
<%- end -%>
|
|
52
80
|
|
|
53
81
|
config.after(:each) do |example|
|
|
54
82
|
example_name = example.description
|