ruby_raider 3.13.0 → 4.0.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/runtime.yml +38 -0
- data/.github/workflows/system_tests.yml +1 -23
- data/.reek.yml +1 -0
- data/.rspec +1 -0
- data/Gemfile +2 -0
- data/README.md +16 -49
- data/RELEASE.md +22 -0
- data/bin/raider +5 -0
- data/lib/commands/scaffolding_commands.rb +2 -25
- data/lib/commands/utility_commands.rb +23 -3
- data/lib/generators/automation/automation_generator.rb +0 -5
- data/lib/generators/automation/templates/component.tt +1 -0
- data/lib/generators/automation/templates/home.tt +2 -2
- data/lib/generators/automation/templates/login.tt +6 -26
- data/lib/generators/automation/templates/page.tt +23 -18
- data/lib/generators/automation/templates/partials/home_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -8
- data/lib/generators/automation/templates/partials/pdp_page_selector.tt +1 -2
- data/lib/generators/automation/templates/partials/url_methods.tt +8 -2
- data/lib/generators/automation/templates/partials/visit_method.tt +0 -5
- data/lib/generators/automation/templates/pdp.tt +1 -9
- data/lib/generators/common_generator.rb +0 -2
- data/lib/generators/generator.rb +12 -19
- data/lib/generators/helper_generator.rb +21 -19
- data/lib/generators/infrastructure/templates/azure_pipelines.tt +1 -1
- data/lib/generators/infrastructure/templates/github.tt +2 -3
- data/lib/generators/infrastructure/templates/github_appium.tt +0 -8
- data/lib/generators/invoke_generators.rb +4 -23
- data/lib/generators/menu_generator.rb +1 -9
- data/lib/generators/rspec/templates/accessibility_spec.tt +1 -9
- data/lib/generators/rspec/templates/spec.tt +8 -24
- data/lib/generators/template_renderer/partial_cache.rb +3 -1
- data/lib/generators/template_renderer.rb +1 -1
- data/lib/generators/templates/common/gemfile.tt +3 -10
- data/lib/generators/templates/common/git_ignore.tt +0 -5
- data/lib/generators/templates/common/partials/mobile_config.tt +3 -1
- data/lib/generators/templates/common/partials/web_config.tt +10 -1
- data/lib/generators/templates/common/rakefile.tt +0 -15
- data/lib/generators/templates/common/read_me.tt +357 -250
- data/lib/generators/templates/common/rubocop.tt +0 -2
- data/lib/generators/templates/helpers/driver_helper.tt +8 -0
- data/lib/generators/templates/helpers/gesture_helper.tt +78 -0
- data/lib/generators/templates/helpers/network_helper.tt +521 -0
- data/lib/generators/templates/helpers/partials/quit_driver.tt +0 -2
- data/lib/generators/templates/helpers/partials/screenshot.tt +0 -2
- data/lib/generators/templates/helpers/partials/selenium_driver.tt +5 -2
- data/lib/generators/templates/helpers/screenshot_helper.tt +490 -0
- data/lib/generators/templates/helpers/spec_helper.tt +39 -29
- data/lib/generators/templates/helpers/trace_helper.tt +240 -95
- data/lib/generators/templates/helpers/video_helper.tt +3 -8
- data/lib/ruby_raider/runtime/actionability.rb +129 -0
- data/lib/ruby_raider/runtime/actionable.rb +49 -0
- data/lib/ruby_raider/runtime/by_role.rb +55 -0
- data/lib/ruby_raider/runtime/config.rb +79 -0
- data/lib/ruby_raider/runtime/context.rb +38 -0
- data/lib/ruby_raider/runtime/errors.rb +69 -0
- data/lib/ruby_raider/runtime/finders.rb +73 -0
- data/lib/ruby_raider/runtime/js/actionability.js +134 -0
- data/lib/ruby_raider/runtime/js/role_engine.js +315 -0
- data/lib/ruby_raider/runtime/locator_converter.rb +72 -0
- data/lib/ruby_raider/runtime/matchers/base.rb +222 -0
- data/lib/ruby_raider/runtime/matchers/element.rb +183 -0
- data/lib/ruby_raider/runtime/matchers/page.rb +71 -0
- data/lib/ruby_raider/runtime/matchers/presence.rb +41 -0
- data/lib/ruby_raider/runtime/matchers/retrying_block.rb +104 -0
- data/lib/ruby_raider/runtime/observer.rb +170 -0
- data/lib/ruby_raider/runtime/role_finder.rb +83 -0
- data/lib/ruby_raider/runtime/rspec.rb +78 -0
- data/lib/ruby_raider/runtime/selectors.rb +60 -0
- data/lib/ruby_raider/runtime/strict.rb +142 -0
- data/lib/ruby_raider/runtime/wait.rb +84 -0
- data/lib/ruby_raider/runtime.rb +20 -0
- data/lib/scaffolding/name_normalizer.rb +1 -1
- data/lib/scaffolding/project_detector.rb +2 -28
- data/lib/scaffolding/scaffolding.rb +0 -20
- data/lib/scaffolding/templates/component.tt +0 -14
- data/lib/scaffolding/templates/page_object.tt +2 -14
- data/lib/scaffolding/templates/spec.tt +1 -9
- data/lib/utilities/utilities.rb +10 -8
- data/lib/version +1 -1
- data/ruby_raider.gemspec +2 -1
- data/sig/commands/scaffolding_commands.rbs +0 -2
- data/sig/commands/utility_commands.rbs +6 -0
- data/sig/generators/generator.rbs +0 -4
- data/sig/generators/helper_generator.rbs +0 -1
- data/sig/generators/menu_generator.rbs +0 -1
- data/sig/ruby_raider/runtime/actionability.rbs +25 -0
- data/sig/ruby_raider/runtime/actionable.rbs +13 -0
- data/sig/ruby_raider/runtime/by_role.rbs +15 -0
- data/sig/ruby_raider/runtime/config.rbs +26 -0
- data/sig/ruby_raider/runtime/context.rbs +12 -0
- data/sig/ruby_raider/runtime/errors.rbs +36 -0
- data/sig/ruby_raider/runtime/finders.rbs +26 -0
- data/sig/ruby_raider/runtime/locator_converter.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/base.rbs +64 -0
- data/sig/ruby_raider/runtime/matchers/element.rbs +78 -0
- data/sig/ruby_raider/runtime/matchers/page.rbs +38 -0
- data/sig/ruby_raider/runtime/matchers/presence.rbs +21 -0
- data/sig/ruby_raider/runtime/matchers/retrying_block.rbs +27 -0
- data/sig/ruby_raider/runtime/observer.rbs +46 -0
- data/sig/ruby_raider/runtime/role_finder.rbs +18 -0
- data/sig/ruby_raider/runtime/selectors.rbs +21 -0
- data/sig/ruby_raider/runtime/strict.rbs +35 -0
- data/sig/ruby_raider/runtime/wait.rbs +17 -0
- data/sig/ruby_raider/runtime.rbs +2 -0
- data/sig/scaffolding/project_detector.rbs +0 -4
- data/sig/scaffolding/scaffolding.rbs +0 -5
- data/sig/utilities/utilities.rbs +2 -1
- data/sig/vendor/selenium_webdriver.rbs +71 -0
- data/spec/commands/raider_commands_spec.rb +38 -3
- data/spec/commands/scaffolding_commands_spec.rb +27 -0
- data/spec/commands/utility_commands_spec.rb +53 -0
- data/spec/generators/gesture_helper_spec.rb +123 -0
- data/spec/generators/network_helper_spec.rb +634 -0
- data/spec/generators/screenshot_helper_spec.rb +776 -0
- data/spec/generators/template_renderer_spec.rb +0 -4
- data/spec/generators/trace_helper_spec.rb +489 -97
- data/spec/generators/video_helper_spec.rb +0 -9
- data/spec/integration/commands/browser_update_after_creation_spec.rb +0 -8
- data/spec/integration/commands/scaffolding_commands_spec.rb +12 -70
- data/spec/integration/commands/utility_commands_spec.rb +19 -41
- data/spec/integration/content/azure_content_spec.rb +2 -6
- data/spec/integration/content/ci_content_spec.rb +1 -55
- data/spec/integration/content/common_content_spec.rb +1 -118
- data/spec/integration/content/config_content_spec.rb +34 -31
- data/spec/integration/content/gemfile_content_spec.rb +7 -72
- data/spec/integration/content/helper_content_spec.rb +105 -99
- data/spec/integration/content/page_content_spec.rb +85 -87
- data/spec/integration/content/readme_content_spec.rb +134 -75
- data/spec/integration/content/rspec_reporter_content_spec.rb +2 -2
- data/spec/integration/content/rubocop_content_spec.rb +1 -1
- data/spec/integration/content/syntax_validation_spec.rb +2 -2
- data/spec/integration/content/test_content_spec.rb +23 -147
- data/spec/integration/end_to_end_features_spec.rb +1 -43
- data/spec/integration/end_to_end_spec.rb +319 -326
- data/spec/integration/generators/automation_generator_spec.rb +3 -37
- data/spec/integration/generators/axe_addon_spec.rb +2 -35
- data/spec/integration/generators/azure_generator_spec.rb +1 -1
- data/spec/integration/generators/common_generator_spec.rb +1 -40
- data/spec/integration/generators/config_features_spec.rb +1 -24
- data/spec/integration/generators/github_generator_spec.rb +0 -12
- data/spec/integration/generators/helpers_generator_spec.rb +74 -80
- data/spec/integration/generators/invoke_generators_spec.rb +54 -45
- data/spec/integration/generators/rspec_generator_spec.rb +0 -6
- data/spec/integration/scaffolding_e2e_spec.rb +17 -179
- data/spec/integration/spec_helper.rb +4 -6
- data/spec/integration/trace_format_checker.rb +11 -2
- data/spec/integration/trace_page_server.rb +72 -5
- data/spec/menus/menu_generator_spec.rb +14 -39
- data/spec/runtime/actionability_spec.rb +207 -0
- data/spec/runtime/actionable_spec.rb +125 -0
- data/spec/runtime/browser/actionability_script_browser_spec.rb +150 -0
- data/spec/runtime/browser/actionable_browser_spec.rb +98 -0
- data/spec/runtime/browser/finders_browser_spec.rb +148 -0
- data/spec/runtime/browser/matchers_browser_spec.rb +67 -0
- data/spec/runtime/browser/retrying_block_browser_spec.rb +37 -0
- data/spec/runtime/browser/role_engine_browser_spec.rb +184 -0
- data/spec/runtime/browser/strict_browser_spec.rb +38 -0
- data/spec/runtime/by_role_spec.rb +52 -0
- data/spec/runtime/config_spec.rb +79 -0
- data/spec/runtime/context_spec.rb +73 -0
- data/spec/runtime/errors_spec.rb +89 -0
- data/spec/runtime/finders_spec.rb +165 -0
- data/spec/runtime/fixtures/actionability.html +70 -0
- data/spec/runtime/fixtures/locators.html +46 -0
- data/spec/runtime/fixtures/matchers.html +33 -0
- data/spec/runtime/fixtures/roles.html +162 -0
- data/spec/runtime/matchers/base_spec.rb +165 -0
- data/spec/runtime/matchers/element_spec.rb +277 -0
- data/spec/runtime/matchers/page_spec.rb +102 -0
- data/spec/runtime/matchers/presence_spec.rb +59 -0
- data/spec/runtime/matchers/retrying_block_spec.rb +165 -0
- data/spec/runtime/observer_spec.rb +341 -0
- data/spec/runtime/role_finder_spec.rb +156 -0
- data/spec/runtime/role_query_spec.rb +75 -0
- data/spec/runtime/selectors_spec.rb +135 -0
- data/spec/runtime/strict_spec.rb +257 -0
- data/spec/runtime/support/browser.rb +48 -0
- data/spec/runtime/support/fake_bridge.rb +117 -0
- data/spec/runtime/wait_spec.rb +71 -0
- data/spec/scaffolding/name_normalizer_spec.rb +3 -2
- data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -43
- data/spec/system/selenium_spec.rb +1 -5
- data/spec/system/support/system_test_helper.rb +2 -5
- data/spec/utilities/headless_config_spec.rb +0 -7
- data/spec/utilities/utilities_spec.rb +24 -0
- metadata +113 -48
- data/lib/generators/automation/templates/header.tt +0 -9
- data/lib/generators/cucumber/cucumber_generator.rb +0 -58
- data/lib/generators/cucumber/templates/accessibility_feature.tt +0 -5
- data/lib/generators/cucumber/templates/accessibility_steps.tt +0 -17
- data/lib/generators/cucumber/templates/cucumber.tt +0 -1
- data/lib/generators/cucumber/templates/env.tt +0 -7
- data/lib/generators/cucumber/templates/feature.tt +0 -21
- data/lib/generators/cucumber/templates/partials/appium_env.tt +0 -26
- data/lib/generators/cucumber/templates/partials/driver_world.tt +0 -18
- data/lib/generators/cucumber/templates/partials/mobile_steps.tt +0 -20
- data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -120
- data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -48
- data/lib/generators/cucumber/templates/partials/watir_world.tt +0 -4
- data/lib/generators/cucumber/templates/partials/web_steps.tt +0 -28
- data/lib/generators/cucumber/templates/steps.tt +0 -5
- data/lib/generators/cucumber/templates/user_factory.tt +0 -15
- data/lib/generators/cucumber/templates/world.tt +0 -7
- data/lib/generators/templates/helpers/browser_helper.tt +0 -31
- data/lib/generators/templates/helpers/raider_element.tt +0 -159
- data/lib/generators/templates/helpers/wait_helper.tt +0 -611
- data/lib/scaffolding/templates/feature.tt +0 -6
- data/lib/scaffolding/templates/steps.tt +0 -19
- data/sig/generators/cucumber/cucumber_generator.rbs +0 -12
- data/spec/fixtures/watir_master/cucumber_watir/Readme.md.snapshot +0 -175
- data/spec/fixtures/watir_master/cucumber_watir/features/step_definitions/login_steps.rb.snapshot +0 -24
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/cucumber_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/Readme.md.snapshot +0 -174
- data/spec/fixtures/watir_master/rspec_watir/page_objects/abstract/page.rb.snapshot +0 -37
- data/spec/fixtures/watir_master/rspec_watir/page_objects/pages/login.rb.snapshot +0 -33
- data/spec/fixtures/watir_master/rspec_watir/spec/login_page_spec.rb.snapshot +0 -49
- data/spec/integration/content/cucumber_reporter_content_spec.rb +0 -161
- data/spec/integration/generators/cucumber_generator_spec.rb +0 -100
- data/spec/integration/helpers/wait_helper_mobile_spec.rb +0 -281
- data/spec/integration/helpers/wait_helper_spec.rb +0 -679
- data/spec/integration/helpers/wait_helper_support.rb +0 -183
- data/spec/system/watir_spec.rb +0 -38
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'selenium-webdriver'
|
|
4
|
-
require 'yaml'
|
|
5
|
-
require_relative 'raider_element'
|
|
6
|
-
|
|
7
|
-
<%- if mobile? -%>
|
|
8
|
-
# Waits before it acts. Each method takes a locator ({ accessibility_id: 'name' },
|
|
9
|
-
# with or without braces, or { ios: {...}, android: {...} }), a RaiderElement or a
|
|
10
|
-
# raw element, polls every 0.1s until the element is ready, then acts on it:
|
|
11
|
-
# tap({ accessibility_id: 'login' })
|
|
12
|
-
# send_keys({ accessibility_id: 'user' }, 'admin')
|
|
13
|
-
# swipe('left', { accessibility_id: 'card' })
|
|
14
|
-
# scroll_to({ accessibility_id: 'footer' })
|
|
15
|
-
# eventually { expect(text(accessibility_id: 'welcome')).to eq('Hi') }
|
|
16
|
-
<%- else -%>
|
|
17
|
-
# Waits before it acts. Each method takes a locator ({ id: 'name' }, with or
|
|
18
|
-
# without braces), a RaiderElement or a raw element, polls every 0.1s until the
|
|
19
|
-
# element is ready, then calls the Selenium method of the same name:
|
|
20
|
-
# click({ id: 'login' })
|
|
21
|
-
# send_keys({ id: 'user' }, 'admin')
|
|
22
|
-
# eventually { expect(text(css: '.welcome')).to eq('Hi') }
|
|
23
|
-
<%- end -%>
|
|
24
|
-
# The wait is config/config.yml `timeout` seconds, or `timeout:` for one call.
|
|
25
|
-
module WaitHelper
|
|
26
|
-
class TimeoutError < Selenium::WebDriver::Error::TimeoutError; end
|
|
27
|
-
|
|
28
|
-
CONFIG_FILE = 'config/config.yml'
|
|
29
|
-
DEFAULT_TIMEOUT = 10
|
|
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
|
-
|
|
54
|
-
def self.default_timeout
|
|
55
|
-
@default_timeout ||= begin
|
|
56
|
-
config = File.exist?(CONFIG_FILE) ? YAML.load_file(CONFIG_FILE) : nil
|
|
57
|
-
(config.is_a?(Hash) && config['timeout']) || DEFAULT_TIMEOUT
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def self.element(driver, locator, by = {})
|
|
62
|
-
RaiderElement.new(driver, Lookup.for(driver, locator, by))
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
<%- if mobile? -%>
|
|
66
|
-
# Swipes or scrolls the element, or the whole screen without a locator.
|
|
67
|
-
def self.gesture(driver, kind, direction, locator, timeout)
|
|
68
|
-
return element(driver, locator).public_send(kind, direction, timeout: timeout) if locator
|
|
69
|
-
|
|
70
|
-
gesture = Gesture.new(driver, kind, direction)
|
|
71
|
-
Waiter.new(driver, timeout: timeout).perform_action(kind, Lookup.screen(driver), Waiter::PRESENT) do
|
|
72
|
-
gesture.perform(nil)
|
|
73
|
-
end
|
|
74
|
-
nil
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
<%- end -%>
|
|
78
|
-
def find_element(locator = nil, visible: true, timeout: nil, **by)
|
|
79
|
-
Waiter.new(driver, timeout: timeout).find_element(Lookup.for(driver, locator, by), visible)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def find_elements(locator = nil, timeout: nil, **by)
|
|
83
|
-
Waiter.new(driver, timeout: timeout).find_elements(Lookup.for(driver, locator, by))
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
<%- if mobile? -%>
|
|
87
|
-
%i[click clear text displayed? enabled? selected?].each do |action|
|
|
88
|
-
<%- else -%>
|
|
89
|
-
%i[click clear submit hover scroll_into_view text displayed? enabled? selected?].each do |action|
|
|
90
|
-
<%- end -%>
|
|
91
|
-
define_method(action) do |locator = nil, timeout: nil, **by|
|
|
92
|
-
WaitHelper.element(driver, locator, by).public_send(action, timeout: timeout)
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
<%- if mobile? -%>
|
|
97
|
-
# tap is Appium's word for click and waits the same way. With a block and no
|
|
98
|
-
# locator it stays Ruby's Object#tap.
|
|
99
|
-
def tap(locator = nil, timeout: nil, **by, &block)
|
|
100
|
-
return super(&block) if block && locator.nil? && by.empty?
|
|
101
|
-
|
|
102
|
-
WaitHelper.element(driver, locator, by).tap(timeout: timeout)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
<%- end -%>
|
|
106
|
-
def send_keys(locator, *keys, timeout: nil)
|
|
107
|
-
WaitHelper.element(driver, locator).send_keys(*keys, timeout: timeout)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
<%- unless mobile? -%>
|
|
111
|
-
def select_by(locator, how, what, timeout: nil)
|
|
112
|
-
WaitHelper.element(driver, locator).select_by(how, what, timeout: timeout)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
<%- end -%>
|
|
116
|
-
def attribute(locator, name, timeout: nil)
|
|
117
|
-
WaitHelper.element(driver, locator).attribute(name, timeout: timeout)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
<%- if mobile? -%>
|
|
121
|
-
# direction: 'up', 'down', 'left' or 'right'.
|
|
122
|
-
def swipe(direction, locator = nil, timeout: nil)
|
|
123
|
-
WaitHelper.gesture(driver, :swipe, direction, locator, timeout)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
def scroll(direction, locator = nil, timeout: nil)
|
|
127
|
-
WaitHelper.gesture(driver, :scroll, direction, locator, timeout)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Scrolls the screen until the element is visible, then returns its RaiderElement.
|
|
131
|
-
def scroll_to(locator, direction: 'down', max_scrolls: 10, timeout: nil)
|
|
132
|
-
WaitHelper.element(driver, locator).scroll_to(direction: direction, max_scrolls: max_scrolls, timeout: timeout)
|
|
133
|
-
end
|
|
134
|
-
<%- else -%>
|
|
135
|
-
def drag_and_drop(source, target, timeout: nil)
|
|
136
|
-
WaitHelper.element(driver, source).drag_and_drop(target, timeout: timeout)
|
|
137
|
-
end
|
|
138
|
-
<%- end -%>
|
|
139
|
-
|
|
140
|
-
def eventually(timeout: nil, &block)
|
|
141
|
-
Waiter.new(nil, timeout: timeout).eventually(&block)
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# How an element is found: a locator in a scope (the driver or a parent
|
|
145
|
-
# Lookup), or an element given as is. The found element is cached until reset.
|
|
146
|
-
class Lookup
|
|
147
|
-
OS_KEYS = %i[android ios].freeze
|
|
148
|
-
PASSED_HINT = 'it was passed in as an element, so it cannot be found again; ' \
|
|
149
|
-
"pass its locator (for example { id: 'name' }) instead"
|
|
150
|
-
MEMBER_HINT = 'it came from find_elements, which keeps no locator per element; call find_elements again to re-query'
|
|
151
|
-
|
|
152
|
-
attr_reader :hint
|
|
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
|
-
|
|
157
|
-
def self.for(driver, locator, by, scope: driver)
|
|
158
|
-
raise ArgumentError, 'pass the locator in braces or as keywords, not both' if locator && !by.empty?
|
|
159
|
-
|
|
160
|
-
given = locator || by
|
|
161
|
-
# A wrapper keeps how it was found, so it can still re-find itself.
|
|
162
|
-
return given.__send__(:lookup) if given.is_a?(RaiderElement)
|
|
163
|
-
return new(scope, element: given, hint: PASSED_HINT) if !given.is_a?(Hash) && given.respond_to?(:displayed?)
|
|
164
|
-
|
|
165
|
-
new(scope, locator: normalize(driver, given))
|
|
166
|
-
end
|
|
167
|
-
<%- if mobile? -%>
|
|
168
|
-
|
|
169
|
-
# No locator and no element: a whole-screen gesture, which locates nothing.
|
|
170
|
-
def self.screen(driver)
|
|
171
|
-
new(driver)
|
|
172
|
-
end
|
|
173
|
-
<%- end -%>
|
|
174
|
-
|
|
175
|
-
def self.normalize(driver, given)
|
|
176
|
-
locator = symbolize(given)
|
|
177
|
-
per_os = !locator.empty? && (locator.keys - OS_KEYS).empty?
|
|
178
|
-
locator = symbolize(locator.fetch(driver.appium_device.to_s.downcase.to_sym, nil), given) if per_os
|
|
179
|
-
return locator if locator.size == 1
|
|
180
|
-
|
|
181
|
-
raise ArgumentError, "a locator is a Hash with one key, such as { id: 'name' }; got #{given.inspect}"
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
def self.symbolize(hash, given = hash)
|
|
185
|
-
return hash.transform_keys(&:to_sym) if hash.is_a?(Hash)
|
|
186
|
-
|
|
187
|
-
raise ArgumentError, "a locator is a Hash with one key, such as { id: 'name' }; got #{given.inspect}"
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def initialize(scope, locator: nil, element: nil, hint: nil, many: false)
|
|
191
|
-
@scope = scope
|
|
192
|
-
@locator = locator
|
|
193
|
-
@element = element
|
|
194
|
-
@last_found = element
|
|
195
|
-
@hint = hint
|
|
196
|
-
@many = many
|
|
197
|
-
@scope_stale = false
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# The last element found. A reset only drops the re-find cache, so a wrapper
|
|
201
|
-
# keeps its element (and its == and hash) after a timed-out wait.
|
|
202
|
-
def element
|
|
203
|
-
@element || @last_found
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def many
|
|
207
|
-
Lookup.new(@scope, locator: @locator, element: @element, many: true)
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def locate
|
|
211
|
-
return find_all if @many
|
|
212
|
-
|
|
213
|
-
return @element if @element || !@locator
|
|
214
|
-
|
|
215
|
-
@element = @last_found = in_scope { |context| context.find_element(**@locator) }
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# A stale element may mean its parent was re-rendered too, so stale resets the chain.
|
|
219
|
-
def reset(stale: false)
|
|
220
|
-
@element = nil if @locator
|
|
221
|
-
@scope_stale = false
|
|
222
|
-
@scope.reset(stale: true) if stale && @scope.is_a?(Lookup)
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
# After a stale error: the Lookup up the chain whose element went stale and cannot be
|
|
226
|
-
# found again (passed in, or from find_elements), or nil when re-finding can heal it.
|
|
227
|
-
def lost
|
|
228
|
-
return self unless @locator
|
|
229
|
-
|
|
230
|
-
@scope.lost if @scope_stale
|
|
231
|
-
end
|
|
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
|
-
|
|
248
|
-
def describe
|
|
249
|
-
return 'the element' unless @locator
|
|
250
|
-
|
|
251
|
-
"{#{@locator.map { |key, value| "#{key}: #{value.inspect}" }.join(', ')}}"
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
private
|
|
255
|
-
|
|
256
|
-
# A stale error while searching inside the parent means the parent, or one above it, went stale.
|
|
257
|
-
def in_scope
|
|
258
|
-
return yield(@scope) unless @scope.is_a?(Lookup)
|
|
259
|
-
|
|
260
|
-
begin
|
|
261
|
-
yield(@scope.locate)
|
|
262
|
-
rescue Selenium::WebDriver::Error::StaleElementReferenceError
|
|
263
|
-
@scope_stale = true
|
|
264
|
-
raise
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def find_all
|
|
269
|
-
return [@element] unless @locator
|
|
270
|
-
|
|
271
|
-
found = in_scope { |context| context.find_elements(**@locator) }
|
|
272
|
-
raise Selenium::WebDriver::Error::NoSuchElementError, 'no match' if found.empty?
|
|
273
|
-
|
|
274
|
-
found
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Polls until a Lookup is ready, then yields the raw element. perform_action
|
|
279
|
-
# is the one choke point every public method (bar eventually) goes through.
|
|
280
|
-
class Waiter
|
|
281
|
-
POLL_INTERVAL = 0.1
|
|
282
|
-
DEADLINE_KEY = :raider_wait_deadline
|
|
283
|
-
VISIBLE_ENABLED = %i[visible enabled].freeze
|
|
284
|
-
PRESENT = [].freeze
|
|
285
|
-
<%- if mobile? -%>
|
|
286
|
-
CLICK = %i[visible enabled stable].freeze
|
|
287
|
-
VISIBLE = %i[visible].freeze
|
|
288
|
-
CHECKS = {
|
|
289
|
-
click: CLICK, tap: CLICK,
|
|
290
|
-
send_keys: VISIBLE_ENABLED, clear: VISIBLE_ENABLED,
|
|
291
|
-
swipe: VISIBLE, scroll: VISIBLE, scroll_to: VISIBLE,
|
|
292
|
-
text: PRESENT, attribute: PRESENT, displayed?: PRESENT, enabled?: PRESENT, selected?: PRESENT
|
|
293
|
-
}.freeze
|
|
294
|
-
<%- else -%>
|
|
295
|
-
CHECKS = {
|
|
296
|
-
click: %i[visible enabled stable].freeze,
|
|
297
|
-
send_keys: VISIBLE_ENABLED, clear: VISIBLE_ENABLED, submit: VISIBLE_ENABLED, select_by: VISIBLE_ENABLED,
|
|
298
|
-
hover: %i[visible stable].freeze,
|
|
299
|
-
drag_and_drop: %i[visible enabled stable].freeze,
|
|
300
|
-
scroll_into_view: PRESENT, text: PRESENT, attribute: PRESENT,
|
|
301
|
-
displayed?: PRESENT, enabled?: PRESENT, selected?: PRESENT
|
|
302
|
-
}.freeze
|
|
303
|
-
<%- end -%>
|
|
304
|
-
TARGET_CHECKS = %i[visible].freeze
|
|
305
|
-
STALE = 'stale element'
|
|
306
|
-
RETRIED = [Selenium::WebDriver::Error::StaleElementReferenceError,
|
|
307
|
-
Selenium::WebDriver::Error::ElementClickInterceptedError,
|
|
308
|
-
Selenium::WebDriver::Error::ElementNotInteractableError].freeze
|
|
309
|
-
HELPER_FILES = [__FILE__, File.expand_path('raider_element.rb', __dir__)].freeze
|
|
310
|
-
|
|
311
|
-
def initialize(driver, timeout: nil)
|
|
312
|
-
@driver = driver
|
|
313
|
-
@timeout = timeout
|
|
314
|
-
@last_rect = nil
|
|
315
|
-
end
|
|
316
|
-
|
|
317
|
-
def find_element(lookup, visible)
|
|
318
|
-
perform_action(:find_element, lookup, visible ? TARGET_CHECKS : PRESENT) { RaiderElement.new(@driver, lookup) }
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
def find_elements(lookup)
|
|
322
|
-
perform_action(:find_elements, lookup.many, PRESENT) do |elements|
|
|
323
|
-
elements.map { |found| RaiderElement.new(@driver, Lookup.new(@driver, element: found, hint: Lookup::MEMBER_HINT)) }
|
|
324
|
-
end
|
|
325
|
-
rescue TimeoutError
|
|
326
|
-
[]
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# checks: a subset of %i[visible enabled stable]; present is always checked.
|
|
330
|
-
# target: a second Lookup (the drop target) that must be visible in the same poll.
|
|
331
|
-
# between: called after each poll that was not ready (scroll_to scrolls there); once it
|
|
332
|
-
# returns false the wait stops, and the timeout message ends with its to_s.
|
|
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)
|
|
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)
|
|
387
|
-
deadline, waited = deadline_and_wait
|
|
388
|
-
loop do
|
|
389
|
-
seen = catch(:not_ready) do
|
|
390
|
-
elements = [ready(lookup, checks)]
|
|
391
|
-
elements << ready(target, TARGET_CHECKS, 'drop target ') if target
|
|
392
|
-
ready_to_act(run, elements.first) if run
|
|
393
|
-
return yield(*elements)
|
|
394
|
-
rescue *RETRIED => e
|
|
395
|
-
retry_reason(lookup, e, action)
|
|
396
|
-
end
|
|
397
|
-
lookup.reset(stale: seen == STALE)
|
|
398
|
-
target&.reset
|
|
399
|
-
if now >= deadline || (between && !between.call)
|
|
400
|
-
seen = [seen, between].compact.join(' ')
|
|
401
|
-
raise TimeoutError, timeout_message(lookup, checks, seen, [waited, waited + now - deadline].min), backtrace
|
|
402
|
-
end
|
|
403
|
-
|
|
404
|
-
run[:retries] += 1 if run
|
|
405
|
-
pause(deadline)
|
|
406
|
-
end
|
|
407
|
-
end
|
|
408
|
-
|
|
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)
|
|
417
|
-
enclosing = Thread.current[DEADLINE_KEY]
|
|
418
|
-
require 'rspec/expectations'
|
|
419
|
-
retried = [RSpec::Expectations::ExpectationNotMetError, Selenium::WebDriver::Error::NoSuchElementError,
|
|
420
|
-
Selenium::WebDriver::Error::StaleElementReferenceError, TimeoutError]
|
|
421
|
-
deadline, waited = deadline_and_wait
|
|
422
|
-
Thread.current[DEADLINE_KEY] = deadline
|
|
423
|
-
loop do
|
|
424
|
-
return yield
|
|
425
|
-
rescue *retried => e
|
|
426
|
-
raise e.exception("(still failing after #{seconds(waited)}s)\n#{e.message}") if now >= deadline
|
|
427
|
-
|
|
428
|
-
retried(run, e)
|
|
429
|
-
pause(deadline)
|
|
430
|
-
end
|
|
431
|
-
ensure
|
|
432
|
-
Thread.current[DEADLINE_KEY] = enclosing
|
|
433
|
-
end
|
|
434
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
# Inside eventually, a call without timeout: waits for what is left of eventually's
|
|
445
|
-
# deadline; one with timeout: waits that long per attempt, never past eventually's deadline.
|
|
446
|
-
def deadline_and_wait
|
|
447
|
-
started = now
|
|
448
|
-
enclosing = Thread.current[DEADLINE_KEY]
|
|
449
|
-
own = started + (@timeout || WaitHelper.default_timeout) if @timeout || !enclosing
|
|
450
|
-
deadline = [own, enclosing].compact.min
|
|
451
|
-
[deadline, deadline - started]
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
def ready(lookup, checks, prefix = '')
|
|
455
|
-
element = lookup.locate
|
|
456
|
-
reason = unready(element, checks)
|
|
457
|
-
throw :not_ready, "#{prefix}#{reason}" if reason
|
|
458
|
-
|
|
459
|
-
element
|
|
460
|
-
rescue Selenium::WebDriver::Error::NoSuchElementError
|
|
461
|
-
throw :not_ready, "#{prefix}not found"
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def unready(element, checks)
|
|
465
|
-
return 'present but not visible' if checks.include?(:visible) && !element.displayed?
|
|
466
|
-
return 'visible but disabled' if checks.include?(:enabled) && !element.enabled?
|
|
467
|
-
|
|
468
|
-
'still moving' if checks.include?(:stable) && !stable?(element)
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
# Stable: the same rect on two consecutive polls.
|
|
472
|
-
def stable?(element)
|
|
473
|
-
rect = element.rect
|
|
474
|
-
steady = rect == @last_rect
|
|
475
|
-
@last_rect = rect
|
|
476
|
-
steady
|
|
477
|
-
end
|
|
478
|
-
|
|
479
|
-
def retry_reason(lookup, error, action)
|
|
480
|
-
detail = error.message.lines.first.to_s.strip
|
|
481
|
-
case error
|
|
482
|
-
when Selenium::WebDriver::Error::StaleElementReferenceError
|
|
483
|
-
lost = lookup.lost
|
|
484
|
-
raise error.class, "#{detail} (during #{action}): #{lost.hint}", backtrace if lost
|
|
485
|
-
|
|
486
|
-
STALE
|
|
487
|
-
when Selenium::WebDriver::Error::ElementClickInterceptedError then "click intercepted: #{detail}"
|
|
488
|
-
else "not interactable: #{detail}"
|
|
489
|
-
end
|
|
490
|
-
end
|
|
491
|
-
|
|
492
|
-
def timeout_message(lookup, checks, seen, waited)
|
|
493
|
-
names = checks.map(&:to_s)
|
|
494
|
-
wanted = names.empty? ? 'present' : [names[0..-2].join(', '), names.last].reject(&:empty?).join(' and ')
|
|
495
|
-
"Waited #{seconds(waited)}s for #{lookup.describe} to be #{wanted}; last seen: #{seen}"
|
|
496
|
-
end
|
|
497
|
-
|
|
498
|
-
def seconds(value)
|
|
499
|
-
rounded = value.round(2)
|
|
500
|
-
rounded == rounded.to_i ? rounded.to_i : rounded
|
|
501
|
-
end
|
|
502
|
-
|
|
503
|
-
# Drops this helper's frames so the error points at the page object or spec line.
|
|
504
|
-
def backtrace
|
|
505
|
-
caller.reject { |line| HELPER_FILES.any? { |file| line.start_with?(file) } }
|
|
506
|
-
.drop_while { |line| line.start_with?('<internal:') }
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
def pause(deadline)
|
|
510
|
-
sleep([[POLL_INTERVAL, deadline - now].min, 0].max)
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
def now
|
|
514
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
515
|
-
end
|
|
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
|
|
549
|
-
<%- if mobile? -%>
|
|
550
|
-
|
|
551
|
-
# A swipe or scroll through the platform's own mobile: command, as appium_lib issues them.
|
|
552
|
-
class Gesture
|
|
553
|
-
DIRECTIONS = %w[up down left right].freeze
|
|
554
|
-
COMMANDS = {
|
|
555
|
-
ios: { swipe: 'mobile: swipe', scroll: 'mobile: scroll' }.freeze,
|
|
556
|
-
android: { swipe: 'mobile: swipeGesture', scroll: 'mobile: scrollGesture' }.freeze
|
|
557
|
-
}.freeze
|
|
558
|
-
PERCENT = 0.75
|
|
559
|
-
|
|
560
|
-
def initialize(driver, kind, direction)
|
|
561
|
-
@driver = driver
|
|
562
|
-
@device = driver.appium_device.to_s.downcase.to_sym
|
|
563
|
-
@command = COMMANDS.dig(@device, kind)
|
|
564
|
-
raise NotImplementedError, "#{kind} has no gesture for #{driver.appium_device.inspect}; only :ios and :android" unless @command
|
|
565
|
-
|
|
566
|
-
@direction = direction.to_s
|
|
567
|
-
raise ArgumentError, "direction must be up, down, left or right; got #{direction.inspect}" unless DIRECTIONS.include?(@direction)
|
|
568
|
-
end
|
|
569
|
-
|
|
570
|
-
# element: the raw element to act on, or nil for the whole screen.
|
|
571
|
-
def perform(element)
|
|
572
|
-
@driver.execute_script(@command, arguments(element))
|
|
573
|
-
end
|
|
574
|
-
|
|
575
|
-
private
|
|
576
|
-
|
|
577
|
-
def arguments(element)
|
|
578
|
-
return { direction: @direction, element: element&.id }.compact if @device == :ios
|
|
579
|
-
|
|
580
|
-
area = element ? { elementId: element.id } : screen
|
|
581
|
-
area.merge(direction: @direction, percent: PERCENT)
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
def screen
|
|
585
|
-
rect = @driver.window_rect
|
|
586
|
-
{ left: rect.x, top: rect.y, width: rect.width, height: rect.height }
|
|
587
|
-
end
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
# Scrolls the screen once per call, between scroll_to's polls, up to max_scrolls times.
|
|
591
|
-
class Scroller
|
|
592
|
-
def initialize(gesture, max_scrolls)
|
|
593
|
-
@gesture = gesture
|
|
594
|
-
@max_scrolls = max_scrolls
|
|
595
|
-
@scrolls = 0
|
|
596
|
-
end
|
|
597
|
-
|
|
598
|
-
# False once every scroll is used up.
|
|
599
|
-
def call
|
|
600
|
-
return false if @scrolls >= @max_scrolls
|
|
601
|
-
|
|
602
|
-
@gesture.perform(nil)
|
|
603
|
-
@scrolls += 1
|
|
604
|
-
end
|
|
605
|
-
|
|
606
|
-
def to_s
|
|
607
|
-
"after #{@scrolls} scrolls"
|
|
608
|
-
end
|
|
609
|
-
end
|
|
610
|
-
<%- end -%>
|
|
611
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative '../../page_objects/pages/<%= normalized_name %>'
|
|
4
|
-
|
|
5
|
-
Given('I am on the <%= normalized_name.tr("_", " ") %> page') do
|
|
6
|
-
<%- if selenium? -%>
|
|
7
|
-
@page = <%= page_class_name %>.new(driver)
|
|
8
|
-
<%- elsif watir? -%>
|
|
9
|
-
@page = <%= page_class_name %>.new(browser)
|
|
10
|
-
<%- end -%>
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
When('I perform an action') do
|
|
14
|
-
pending 'implement step'
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
Then('I see the expected result') do
|
|
18
|
-
pending 'implement step'
|
|
19
|
-
end
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# Type definitions for CucumberGenerator
|
|
2
|
-
class CucumberGenerator < Generator
|
|
3
|
-
def generate_feature: () -> void
|
|
4
|
-
def generate_steps: () -> void
|
|
5
|
-
def generate_user_factory: () -> void
|
|
6
|
-
def generate_env_file: () -> void
|
|
7
|
-
def generate_world: () -> void
|
|
8
|
-
def generate_cucumber_file: () -> void
|
|
9
|
-
def generate_accessibility_feature: () -> void
|
|
10
|
-
def generate_accessibility_steps: () -> void
|
|
11
|
-
def template_name: () -> String
|
|
12
|
-
end
|