ruby_raider 3.12.1 → 3.13.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/README.md +2 -0
- data/lib/commands/utility_commands.rb +16 -0
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +65 -1
- data/lib/generators/generator.rb +10 -1
- data/lib/generators/helper_generator.rb +7 -0
- data/lib/generators/templates/common/gemfile.tt +1 -1
- data/lib/generators/templates/common/partials/web_config.tt +7 -0
- data/lib/generators/templates/common/read_me.tt +45 -0
- data/lib/generators/templates/helpers/driver_helper.tt +22 -0
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +11 -1
- data/lib/generators/templates/helpers/raider_element.tt +4 -4
- data/lib/generators/templates/helpers/spec_helper.tt +19 -0
- data/lib/generators/templates/helpers/trace_helper.tt +1097 -0
- data/lib/generators/templates/helpers/wait_helper.tt +145 -4
- data/lib/utilities/utilities.rb +7 -0
- data/lib/version +1 -1
- data/ruby_raider.gemspec +1 -0
- data/sig/commands/utility_commands.rbs +2 -0
- data/sig/generators/generator.rbs +4 -0
- data/sig/generators/helper_generator.rbs +1 -0
- data/sig/utilities/utilities.rbs +1 -0
- data/spec/generators/fixtures/video_helper_stubs/selenium-webdriver.rb +11 -2
- data/spec/generators/trace_helper_spec.rb +945 -0
- data/spec/integration/content/config_content_spec.rb +20 -0
- data/spec/integration/content/cucumber_reporter_content_spec.rb +51 -0
- data/spec/integration/content/gemfile_content_spec.rb +2 -2
- data/spec/integration/content/helper_content_spec.rb +60 -1
- data/spec/integration/content/readme_content_spec.rb +46 -0
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/end_to_end_spec.rb +392 -29
- data/spec/integration/generators/helpers_generator_spec.rb +26 -0
- data/spec/integration/helpers/wait_helper_spec.rb +183 -0
- data/spec/integration/trace_format_checker.rb +141 -0
- data/spec/integration/trace_page_server.rb +80 -0
- data/spec/utilities/trace_command_spec.rb +66 -0
- metadata +27 -2
|
@@ -28,6 +28,29 @@ module WaitHelper
|
|
|
28
28
|
CONFIG_FILE = 'config/config.yml'
|
|
29
29
|
DEFAULT_TIMEOUT = 10
|
|
30
30
|
|
|
31
|
+
class << self
|
|
32
|
+
# Follows every action and eventually when set (the trace helper does). Optional:
|
|
33
|
+
# nothing is called on it while nil, and one that raises is dropped.
|
|
34
|
+
attr_accessor :observer
|
|
35
|
+
|
|
36
|
+
# Calls the observer's hook, or returns nil without one. A broken observer must
|
|
37
|
+
# never fail a test: it is warned about once and removed.
|
|
38
|
+
# A block builds the details, so computing them is covered by the same rescue.
|
|
39
|
+
# $stderr.puts, not Kernel#warn: warn is silenced under -W0.
|
|
40
|
+
# rubocop:disable Style/StderrPuts
|
|
41
|
+
def report(hook, *args, **details)
|
|
42
|
+
current = observer
|
|
43
|
+
return unless current
|
|
44
|
+
|
|
45
|
+
details = yield if block_given?
|
|
46
|
+
current.public_send(hook, *args, **details)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
$stderr.puts("[trace] observer stopped: #{e.class}: #{e.message}")
|
|
49
|
+
self.observer = nil
|
|
50
|
+
end
|
|
51
|
+
# rubocop:enable Style/StderrPuts
|
|
52
|
+
end
|
|
53
|
+
|
|
31
54
|
def self.default_timeout
|
|
32
55
|
@default_timeout ||= begin
|
|
33
56
|
config = File.exist?(CONFIG_FILE) ? YAML.load_file(CONFIG_FILE) : nil
|
|
@@ -128,6 +151,9 @@ module WaitHelper
|
|
|
128
151
|
|
|
129
152
|
attr_reader :hint
|
|
130
153
|
|
|
154
|
+
# Selenium's aliases for the locator strategies, as the trace names them.
|
|
155
|
+
STRATEGIES = { css_selector: 'css', class: 'class_name', link: 'link_text', partial_link: 'partial_link_text' }.freeze
|
|
156
|
+
|
|
131
157
|
def self.for(driver, locator, by, scope: driver)
|
|
132
158
|
raise ArgumentError, 'pass the locator in braces or as keywords, not both' if locator && !by.empty?
|
|
133
159
|
|
|
@@ -204,6 +230,21 @@ module WaitHelper
|
|
|
204
230
|
@scope.lost if @scope_stale
|
|
205
231
|
end
|
|
206
232
|
|
|
233
|
+
# { strategy:, selector: } for the observer, or nil when this locates nothing.
|
|
234
|
+
def trace_locator
|
|
235
|
+
return unless @locator
|
|
236
|
+
|
|
237
|
+
strategy, selector = @locator.first
|
|
238
|
+
{ strategy: STRATEGIES.fetch(strategy) { strategy.to_s }, selector: selector }
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# The locators this one is searched inside, outermost first.
|
|
242
|
+
def trace_within
|
|
243
|
+
return [] unless @scope.is_a?(Lookup)
|
|
244
|
+
|
|
245
|
+
[*@scope.trace_within, @scope.trace_locator].compact
|
|
246
|
+
end
|
|
247
|
+
|
|
207
248
|
def describe
|
|
208
249
|
return 'the element' unless @locator
|
|
209
250
|
|
|
@@ -289,13 +330,66 @@ module WaitHelper
|
|
|
289
330
|
# target: a second Lookup (the drop target) that must be visible in the same poll.
|
|
290
331
|
# between: called after each poll that was not ready (scroll_to scrolls there); once it
|
|
291
332
|
# returns false the wait stops, and the timeout message ends with its to_s.
|
|
292
|
-
|
|
333
|
+
# args: what the action acts with (send_keys' keys, select_by's how and what), for the observer.
|
|
334
|
+
def perform_action(action, locator, checks, target: nil, between: nil, args: nil, &block)
|
|
293
335
|
lookup = locator.is_a?(Lookup) ? locator : Lookup.for(@driver, locator, {})
|
|
336
|
+
return poll(action, lookup, checks, target, between, nil, &block) unless WaitHelper.observer
|
|
337
|
+
|
|
338
|
+
observed(action, lookup, args) { |run| poll(action, lookup, checks, target, between, run, &block) }
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Retries the block until it passes. Calls inside it share its deadline (see deadline_and_wait).
|
|
342
|
+
def eventually(&block)
|
|
343
|
+
return retry_block(nil, &block) unless WaitHelper.observer
|
|
344
|
+
|
|
345
|
+
token = WaitHelper.report(:group_started) { { action: :eventually, source: Observation.source } }
|
|
346
|
+
run = { retries: 0, token: token }
|
|
347
|
+
begin
|
|
348
|
+
result = retry_block(run, &block)
|
|
349
|
+
WaitHelper.report(:group_finished, token, status: :passed, retries: run[:retries], error: nil) if token
|
|
350
|
+
result
|
|
351
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
352
|
+
WaitHelper.report(:group_finished, token, status: :failed, retries: run[:retries], error: e) if token
|
|
353
|
+
raise
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
private
|
|
358
|
+
|
|
359
|
+
# Tells the observer an action started, when its element is ready and how it ended. The
|
|
360
|
+
# block gets a run hash that poll fills with the retries it took and the ready element.
|
|
361
|
+
def observed(action, lookup, args)
|
|
362
|
+
run = { retries: 0, element: nil, token: nil }
|
|
363
|
+
token = run[:token] = WaitHelper.report(:action_started) do
|
|
364
|
+
{ action: action, locator: lookup.trace_locator, within: lookup.trace_within,
|
|
365
|
+
source: Observation.source, args: args }
|
|
366
|
+
end
|
|
367
|
+
started = now
|
|
368
|
+
begin
|
|
369
|
+
result = yield(run)
|
|
370
|
+
finished(token, :passed, started, run, nil)
|
|
371
|
+
result
|
|
372
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
373
|
+
finished(token, :failed, started, run, e)
|
|
374
|
+
raise
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def finished(token, status, started, run, error)
|
|
379
|
+
return unless token
|
|
380
|
+
|
|
381
|
+
WaitHelper.report(:action_finished, token, status: status, waited_ms: ((now - started) * 1000).round,
|
|
382
|
+
retries: run[:retries], error: error, element: run[:element])
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# run: where to count retries and keep the ready element, or nil when nobody observes.
|
|
386
|
+
def poll(action, lookup, checks, target, between, run)
|
|
294
387
|
deadline, waited = deadline_and_wait
|
|
295
388
|
loop do
|
|
296
389
|
seen = catch(:not_ready) do
|
|
297
390
|
elements = [ready(lookup, checks)]
|
|
298
391
|
elements << ready(target, TARGET_CHECKS, 'drop target ') if target
|
|
392
|
+
ready_to_act(run, elements.first) if run
|
|
299
393
|
return yield(*elements)
|
|
300
394
|
rescue *RETRIED => e
|
|
301
395
|
retry_reason(lookup, e, action)
|
|
@@ -307,12 +401,19 @@ module WaitHelper
|
|
|
307
401
|
raise TimeoutError, timeout_message(lookup, checks, seen, [waited, waited + now - deadline].min), backtrace
|
|
308
402
|
end
|
|
309
403
|
|
|
404
|
+
run[:retries] += 1 if run
|
|
310
405
|
pause(deadline)
|
|
311
406
|
end
|
|
312
407
|
end
|
|
313
408
|
|
|
314
|
-
#
|
|
315
|
-
def
|
|
409
|
+
# Keeps the ready element and tells the observer, just before the action runs on it.
|
|
410
|
+
def ready_to_act(run, element)
|
|
411
|
+
run[:element] = element
|
|
412
|
+
token = run[:token]
|
|
413
|
+
WaitHelper.report(:action_ready, token, element: element) if token
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def retry_block(run)
|
|
316
417
|
enclosing = Thread.current[DEADLINE_KEY]
|
|
317
418
|
require 'rspec/expectations'
|
|
318
419
|
retried = [RSpec::Expectations::ExpectationNotMetError, Selenium::WebDriver::Error::NoSuchElementError,
|
|
@@ -324,13 +425,21 @@ module WaitHelper
|
|
|
324
425
|
rescue *retried => e
|
|
325
426
|
raise e.exception("(still failing after #{seconds(waited)}s)\n#{e.message}") if now >= deadline
|
|
326
427
|
|
|
428
|
+
retried(run, e)
|
|
327
429
|
pause(deadline)
|
|
328
430
|
end
|
|
329
431
|
ensure
|
|
330
432
|
Thread.current[DEADLINE_KEY] = enclosing
|
|
331
433
|
end
|
|
332
434
|
|
|
333
|
-
|
|
435
|
+
# Counts a failed pass of eventually, and tells the observer a new pass starts.
|
|
436
|
+
def retried(run, error)
|
|
437
|
+
return unless run
|
|
438
|
+
|
|
439
|
+
run[:retries] += 1
|
|
440
|
+
token = run[:token]
|
|
441
|
+
WaitHelper.report(:group_retried, token, error: error) if token
|
|
442
|
+
end
|
|
334
443
|
|
|
335
444
|
# Inside eventually, a call without timeout: waits for what is left of eventually's
|
|
336
445
|
# deadline; one with timeout: waits that long per attempt, never past eventually's deadline.
|
|
@@ -405,6 +514,38 @@ module WaitHelper
|
|
|
405
514
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
406
515
|
end
|
|
407
516
|
end
|
|
517
|
+
|
|
518
|
+
# Where an action was called from, for the observer.
|
|
519
|
+
module Observation
|
|
520
|
+
# helpers/ by its given and its real path (__dir__ is the real one): the wait, trace and
|
|
521
|
+
# driver helpers are never the line that called an action or a navigation.
|
|
522
|
+
HELPER_DIRS = [File.dirname(__FILE__), __dir__].uniq.map { |dir| File.join(dir, '') }.freeze
|
|
523
|
+
|
|
524
|
+
# The first frame outside helpers/, Ruby and gems, as { file:, line: } (file relative
|
|
525
|
+
# to the working directory when inside it), or nil.
|
|
526
|
+
def self.source
|
|
527
|
+
frame = caller_locations.find { |location| project?(location.absolute_path || location.path) }
|
|
528
|
+
return unless frame
|
|
529
|
+
|
|
530
|
+
{ file: relative(frame.absolute_path || frame.path), line: frame.lineno }
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def self.project?(path)
|
|
534
|
+
return false if path.start_with?('<internal:') || HELPER_DIRS.any? { |dir| path.start_with?(dir) }
|
|
535
|
+
|
|
536
|
+
library_paths.none? { |root| path.start_with?(root) }
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def self.library_paths
|
|
540
|
+
@library_paths ||= [*RbConfig::CONFIG.values_at('rubylibdir', 'rubyarchdir'), *Gem.path]
|
|
541
|
+
.compact.map { |dir| File.join(dir, '') }
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def self.relative(path)
|
|
545
|
+
root = File.join(Dir.pwd, '')
|
|
546
|
+
path.start_with?(root) ? path.delete_prefix(root) : path
|
|
547
|
+
end
|
|
548
|
+
end
|
|
408
549
|
<%- if mobile? -%>
|
|
409
550
|
|
|
410
551
|
# A swipe or scroll through the platform's own mobile: command, as appium_lib issues them.
|
data/lib/utilities/utilities.rb
CHANGED
|
@@ -75,6 +75,13 @@ module Utilities
|
|
|
75
75
|
overwrite_yaml
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
def trace_enabled=(enabled)
|
|
79
|
+
reload_config
|
|
80
|
+
trace = @config['trace']
|
|
81
|
+
@config['trace'] = (trace.is_a?(Hash) ? trace : {}).merge('enabled' => enabled)
|
|
82
|
+
overwrite_yaml
|
|
83
|
+
end
|
|
84
|
+
|
|
78
85
|
# Set multiple config keys in a single YAML write.
|
|
79
86
|
# Usage: Utilities.batch_update(browser: 'chrome', timeout: 30)
|
|
80
87
|
def batch_update(**settings)
|
data/lib/version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.13.0
|
data/ruby_raider.gemspec
CHANGED
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
|
|
|
21
21
|
s.add_development_dependency 'rubocop-performance', '~> 1.15.0'
|
|
22
22
|
s.add_development_dependency 'rubocop-rspec', '~> 2.9.0'
|
|
23
23
|
s.add_development_dependency 'ruby_audit', '~> 3.1'
|
|
24
|
+
s.add_development_dependency 'rubyzip', '>= 2.3', '< 4'
|
|
24
25
|
s.add_development_dependency 'selenium-webdriver', '~> 4.0'
|
|
25
26
|
s.add_development_dependency 'steep', '~> 1.9'
|
|
26
27
|
|
|
@@ -20,6 +20,7 @@ class Generator < Thor::Group
|
|
|
20
20
|
def selenium?: () -> bool
|
|
21
21
|
def watir?: () -> bool
|
|
22
22
|
def wait_helper?: () -> bool
|
|
23
|
+
def trace_helper?: () -> bool
|
|
23
24
|
def examples?: () -> bool
|
|
24
25
|
def web?: () -> bool
|
|
25
26
|
def selenium_based?: () -> bool
|
|
@@ -27,6 +28,9 @@ class Generator < Thor::Group
|
|
|
27
28
|
# Add-on predicates
|
|
28
29
|
def axe_addon?: () -> bool
|
|
29
30
|
|
|
31
|
+
def ruby_raider_version: () -> String
|
|
32
|
+
def ruby_raider_version_requirement: () -> String
|
|
33
|
+
|
|
30
34
|
private
|
|
31
35
|
|
|
32
36
|
def options_after_name: () -> Array[untyped]
|
data/sig/utilities/utilities.rbs
CHANGED
|
@@ -14,6 +14,7 @@ module Utilities
|
|
|
14
14
|
def self?.browser_options=: (Array[String] opts) -> void
|
|
15
15
|
def self?.delete_browser_options: () -> void
|
|
16
16
|
def self?.video_enabled=: (bool enabled) -> void
|
|
17
|
+
def self?.trace_enabled=: (bool enabled) -> void
|
|
17
18
|
def self?.batch_update: (**untyped settings) -> void
|
|
18
19
|
def self?.run: (?String? opts) -> void
|
|
19
20
|
def self?.parallel_run: (?String? opts, ?untyped? _settings) -> void
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Minimal stand-in for selenium-webdriver, which is not in ruby_raider's bundle.
|
|
4
|
-
# spec/generators/video_helper_spec.rb
|
|
5
|
-
# rendered
|
|
4
|
+
# spec/generators/video_helper_spec.rb and trace_helper_spec.rb put this directory on
|
|
5
|
+
# $LOAD_PATH so the rendered helpers can `require 'selenium-webdriver'`. The listener
|
|
6
|
+
# lists every callback of the real Support::AbstractEventListener (4.52).
|
|
6
7
|
module Selenium
|
|
7
8
|
module WebDriver
|
|
8
9
|
module Error
|
|
@@ -14,6 +15,10 @@ module Selenium
|
|
|
14
15
|
class AbstractEventListener
|
|
15
16
|
def before_navigate_to(url, driver) end
|
|
16
17
|
def after_navigate_to(url, driver) end
|
|
18
|
+
def before_navigate_back(driver) end
|
|
19
|
+
def after_navigate_back(driver) end
|
|
20
|
+
def before_navigate_forward(driver) end
|
|
21
|
+
def after_navigate_forward(driver) end
|
|
17
22
|
def before_find(by, what, driver) end
|
|
18
23
|
def after_find(by, what, driver) end
|
|
19
24
|
def before_click(element, driver) end
|
|
@@ -22,6 +27,10 @@ module Selenium
|
|
|
22
27
|
def after_change_value_of(element, driver) end
|
|
23
28
|
def before_execute_script(script, driver) end
|
|
24
29
|
def after_execute_script(script, driver) end
|
|
30
|
+
def before_quit(driver) end
|
|
31
|
+
def after_quit(driver) end
|
|
32
|
+
def before_close(driver) end
|
|
33
|
+
def after_close(driver) end
|
|
25
34
|
end
|
|
26
35
|
end
|
|
27
36
|
end
|