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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Raider
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matchers that read one element. Readers go through read_with_zero_wait so a
|
|
8
|
+
# template element (which takes timeout:) never waits inside the matcher's own wait.
|
|
9
|
+
module Element
|
|
10
|
+
# Collapses runs of whitespace so text compares the way it reads on the page.
|
|
11
|
+
def self.normalise(text)
|
|
12
|
+
text.to_s.strip.gsub(/\s+/, ' ')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# The text of an element, as a substring or (exact: true) as equality.
|
|
16
|
+
class HaveText < Base
|
|
17
|
+
def initialize(text, exact: false, wait: nil)
|
|
18
|
+
super(text, wait: wait, exact: exact)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def verb
|
|
22
|
+
option(:exact) ? 'have exact text' : 'have text'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read(subject)
|
|
26
|
+
read_with_zero_wait(subject, :text)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pass?(value)
|
|
30
|
+
actual = Element.normalise(value)
|
|
31
|
+
return expected.match?(actual) if expected.is_a?(Regexp)
|
|
32
|
+
|
|
33
|
+
wanted = Element.normalise(expected)
|
|
34
|
+
option(:exact) ? actual == wanted : actual.include?(wanted)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The `value` attribute of a field.
|
|
39
|
+
class HaveValue < Base
|
|
40
|
+
def verb
|
|
41
|
+
'have value'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read(subject)
|
|
45
|
+
read_with_zero_wait(subject, :attribute, 'value')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def pass?(value)
|
|
49
|
+
expected.is_a?(Regexp) ? expected.match?(value.to_s) : value.to_s == expected.to_s
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# An attribute that is present, equal to a String or matching a Regexp.
|
|
54
|
+
class HaveAttribute < Base
|
|
55
|
+
PRESENT = :present
|
|
56
|
+
|
|
57
|
+
def initialize(name, value = PRESENT, wait: nil)
|
|
58
|
+
super(name, wait: wait, value: value)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def verb
|
|
62
|
+
'have attribute'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def description
|
|
66
|
+
wanted = option(:value)
|
|
67
|
+
wanted == PRESENT ? super : "#{verb} #{expected.inspect} #{wanted.inspect}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read(subject)
|
|
71
|
+
read_with_zero_wait(subject, :attribute, expected)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def pass?(value)
|
|
75
|
+
wanted = option(:value)
|
|
76
|
+
return !value.nil? if wanted == PRESENT
|
|
77
|
+
return false if value.nil?
|
|
78
|
+
|
|
79
|
+
wanted.is_a?(Regexp) ? wanted.match?(value.to_s) : value.to_s == wanted.to_s
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# A boolean state of an element, worded by the two labels it can read as.
|
|
84
|
+
class State < Base
|
|
85
|
+
LABELS = %w[yes no].freeze
|
|
86
|
+
|
|
87
|
+
def initialize(wait: nil)
|
|
88
|
+
super(true, wait: wait)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def description
|
|
92
|
+
verb
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def pass?(value)
|
|
96
|
+
value == true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def show(value)
|
|
102
|
+
value ? self.class::LABELS.first : self.class::LABELS.last
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Displayed on the page.
|
|
107
|
+
class BeVisible < State
|
|
108
|
+
LABELS = %w[visible hidden].freeze
|
|
109
|
+
|
|
110
|
+
def verb
|
|
111
|
+
'be visible'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def read(subject)
|
|
115
|
+
read_with_zero_wait(subject, :displayed?)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Gone counts as not visible for the negated form.
|
|
121
|
+
def negated_absent_passes?
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Not displayed, or gone: absence is a pass for the positive form.
|
|
127
|
+
class BeHidden < State
|
|
128
|
+
LABELS = %w[hidden visible].freeze
|
|
129
|
+
|
|
130
|
+
def verb
|
|
131
|
+
'be hidden'
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def absent_passes?
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def read(subject)
|
|
139
|
+
!read_with_zero_wait(subject, :displayed?)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Enabled for interaction.
|
|
144
|
+
class BeEnabled < State
|
|
145
|
+
LABELS = %w[enabled disabled].freeze
|
|
146
|
+
|
|
147
|
+
def verb
|
|
148
|
+
'be enabled'
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def read(subject)
|
|
152
|
+
read_with_zero_wait(subject, :enabled?)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Not enabled.
|
|
157
|
+
class BeDisabled < State
|
|
158
|
+
LABELS = %w[disabled enabled].freeze
|
|
159
|
+
|
|
160
|
+
def verb
|
|
161
|
+
'be disabled'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def read(subject)
|
|
165
|
+
!read_with_zero_wait(subject, :enabled?)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# A checked checkbox or radio, or a selected option.
|
|
170
|
+
class BeChecked < State
|
|
171
|
+
LABELS = %w[checked unchecked].freeze
|
|
172
|
+
|
|
173
|
+
def verb
|
|
174
|
+
'be checked'
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def read(subject)
|
|
178
|
+
read_with_zero_wait(subject, :selected?)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Raider
|
|
6
|
+
module Matchers
|
|
7
|
+
# Matchers about the page or a collection rather than one element.
|
|
8
|
+
module Page
|
|
9
|
+
# The size of an Array (read once) or of what a block returns (re-called each poll).
|
|
10
|
+
class HaveCount < Base
|
|
11
|
+
def verb
|
|
12
|
+
'have count'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def read(subject)
|
|
16
|
+
subject.size
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pass?(value)
|
|
20
|
+
value == expected
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# A collection already in hand cannot change, so it gets a single pass.
|
|
26
|
+
def wait_for(actual)
|
|
27
|
+
actual.is_a?(Array) ? 0 : super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def target
|
|
31
|
+
polled_actual.is_a?(Array) ? 'the array' : super
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A value read from the driver: a String is equality, a Regexp is match?.
|
|
36
|
+
class Driver < Base
|
|
37
|
+
def pass?(value)
|
|
38
|
+
expected.is_a?(Regexp) ? expected.match?(value.to_s) : value.to_s == expected.to_s
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def target
|
|
44
|
+
'the page'
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The title of the page.
|
|
49
|
+
class HaveTitle < Driver
|
|
50
|
+
def verb
|
|
51
|
+
'have title'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def read(subject)
|
|
55
|
+
subject.title
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The current url of the page, whole: 'account' does not match '.../account'.
|
|
60
|
+
class HaveUrl < Driver
|
|
61
|
+
def verb
|
|
62
|
+
'have url'
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def read(subject)
|
|
66
|
+
subject.current_url
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../strict'
|
|
5
|
+
|
|
6
|
+
module Raider
|
|
7
|
+
module Matchers
|
|
8
|
+
# Matchers about whether elements exist under a driver or element.
|
|
9
|
+
module Presence
|
|
10
|
+
# The locator matches at least one element (negated: none). `expected` is the locator.
|
|
11
|
+
class HaveElement < Base
|
|
12
|
+
def verb
|
|
13
|
+
'have element'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Base#settled? already reads under Wait.immediately.
|
|
17
|
+
def read(subject)
|
|
18
|
+
subject.find_elements(expected)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def pass?(value)
|
|
22
|
+
!value.empty?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def description
|
|
26
|
+
"#{verb} #{Raider::Locator.describe([expected])}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def target
|
|
32
|
+
polled_actual.is_a?(Selenium::WebDriver::Driver) ? 'the page' : super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def show(value)
|
|
36
|
+
value.size
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec/expectations'
|
|
4
|
+
require 'selenium-webdriver'
|
|
5
|
+
require_relative '../config'
|
|
6
|
+
require_relative '../errors'
|
|
7
|
+
require_relative '../wait'
|
|
8
|
+
require_relative 'base'
|
|
9
|
+
|
|
10
|
+
module Raider
|
|
11
|
+
module Matchers
|
|
12
|
+
# Prepended to RSpec's BlockExpectationTarget: `expect { value }.to eq(x)` polls the
|
|
13
|
+
# block until the value matches. Matchers that handle blocks themselves (Raider's,
|
|
14
|
+
# raise_error, change, output, yield_*) keep stock semantics and one evaluation.
|
|
15
|
+
# A polled block is re-run on every poll, so side effects in it repeat.
|
|
16
|
+
# Under aggregate_failures RSpec's notifier records the failure without raising,
|
|
17
|
+
# so the "(waited Ns)" prefix does not appear there.
|
|
18
|
+
module RetryingBlock
|
|
19
|
+
LOOKUP_ERRORS = [Selenium::WebDriver::Error::NoSuchElementError,
|
|
20
|
+
Selenium::WebDriver::Error::StaleElementReferenceError,
|
|
21
|
+
Raider::AmbiguousMatch,
|
|
22
|
+
Selenium::WebDriver::Error::TimeoutError].freeze
|
|
23
|
+
|
|
24
|
+
# One evaluation of the block and the check; remembers what it saw.
|
|
25
|
+
class Attempt
|
|
26
|
+
attr_reader :value, :error
|
|
27
|
+
|
|
28
|
+
def initialize(block, check)
|
|
29
|
+
@block = block
|
|
30
|
+
@check = check
|
|
31
|
+
@value = Base::NOT_FOUND
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def read?
|
|
35
|
+
!@value.equal?(Base::NOT_FOUND)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def pass?
|
|
39
|
+
@error = nil
|
|
40
|
+
@value = Raider::Wait.immediately { @block.call }
|
|
41
|
+
@check.call(@value)
|
|
42
|
+
rescue *LOOKUP_ERRORS => e
|
|
43
|
+
@error = e
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def to(matcher = nil, message = nil, &)
|
|
49
|
+
return super if stock?(matcher)
|
|
50
|
+
|
|
51
|
+
settle(matcher, message, :to, false, &)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def not_to(matcher = nil, message = nil, &)
|
|
55
|
+
return super if stock?(matcher)
|
|
56
|
+
|
|
57
|
+
settle(matcher, message, :not_to, true, &)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_not(...)
|
|
61
|
+
not_to(...)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def stock?(matcher)
|
|
67
|
+
matcher.nil? || matcher.is_a?(Raider::Matchers::Base) ||
|
|
68
|
+
(matcher.respond_to?(:supports_block_expectations?) && matcher.supports_block_expectations?)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def check(matcher, negated)
|
|
72
|
+
return matcher.method(:matches?) unless negated
|
|
73
|
+
return matcher.method(:does_not_match?) if matcher.respond_to?(:does_not_match?)
|
|
74
|
+
|
|
75
|
+
->(value) { !matcher.matches?(value) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def settle(matcher, message, polarity, negated, &)
|
|
79
|
+
attempt = Attempt.new(target, check(matcher, negated))
|
|
80
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
81
|
+
return true if polled?(attempt)
|
|
82
|
+
|
|
83
|
+
waited = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started
|
|
84
|
+
give_up(attempt, [matcher, message], polarity, waited, &)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def polled?(attempt)
|
|
88
|
+
Raider::Wait.until(timeout: Raider.timeout) { attempt.pass? || throw(:not_ready) }
|
|
89
|
+
true
|
|
90
|
+
rescue Raider::TimeoutError
|
|
91
|
+
false
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The last poll's error wins; the last value goes to stock RSpec only when the last poll read one.
|
|
95
|
+
def give_up(attempt, arguments, polarity, waited, &)
|
|
96
|
+
raise(attempt.error || Raider::TimeoutError.new('the block never produced a value')) if attempt.error || !attempt.read?
|
|
97
|
+
|
|
98
|
+
RSpec::Expectations::ExpectationTarget.for(attempt.value, nil).public_send(polarity, *arguments, &)
|
|
99
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
|
100
|
+
raise e.exception("(waited #{Raider::Describe.seconds(waited)}s) #{e.message}"), cause: nil
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'selenium-webdriver'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
require_relative 'role_finder'
|
|
6
|
+
|
|
7
|
+
# Reports to an optional observer. A failing observer is switched off, never the test.
|
|
8
|
+
module Raider
|
|
9
|
+
class << self
|
|
10
|
+
# Calls hook on the observer when it answers it; the block builds the details lazily.
|
|
11
|
+
# A broken observer (even a NotImplementedError or SystemStackError) is switched off, never the test.
|
|
12
|
+
def report(hook, *args, **details)
|
|
13
|
+
watcher = observer
|
|
14
|
+
return unless watcher.respond_to?(hook)
|
|
15
|
+
|
|
16
|
+
Observer.guarded { watcher.public_send(hook, *args, **(block_given? ? yield : details)) }
|
|
17
|
+
rescue StandardError, ScriptError, SystemStackError => e
|
|
18
|
+
Observer.stop(watcher, e)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Element actions as events, and the locator stamped on every found element.
|
|
23
|
+
module Observer
|
|
24
|
+
LIB_DIR = File.expand_path(__dir__)
|
|
25
|
+
GUARD_KEY = :raider_in_observer
|
|
26
|
+
MESSAGE_LIMIT = 200
|
|
27
|
+
|
|
28
|
+
STRATEGIES = {
|
|
29
|
+
'css_selector' => 'css', 'css selector' => 'css', 'class' => 'class_name', 'class name' => 'class_name',
|
|
30
|
+
'link' => 'link_text', 'link text' => 'link_text', 'partial_link' => 'partial_link_text',
|
|
31
|
+
'partial link text' => 'partial_link_text', 'tag name' => 'tag_name'
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
# One reported action. :reek:TooManyInstanceVariables { max_instance_variables: 6 } -- the event's whole state
|
|
35
|
+
class Run
|
|
36
|
+
def initialize(element, action, args)
|
|
37
|
+
@element = element
|
|
38
|
+
@action = action
|
|
39
|
+
@args = args&.dup&.freeze # an observer can read the keys, never change what gets typed
|
|
40
|
+
@retries = 0
|
|
41
|
+
@token = Raider.report(:action_started) { Observer.started(element, action, @args) }
|
|
42
|
+
@started = Observer.clock
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ready(retries)
|
|
46
|
+
@retries = retries
|
|
47
|
+
Raider.report(:action_ready, @token) { { element: @element } } unless @token.nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def finish(error)
|
|
51
|
+
Raider.report(:action_finished, @token) { finished(error) } unless @token.nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def finished(error)
|
|
57
|
+
@retries = error.retries if error.is_a?(NotActionable)
|
|
58
|
+
{ status: error ? :failed : :passed, waited_ms: Observer.millis(@started), retries: @retries,
|
|
59
|
+
error: error, element: @element }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
# Yields a Run (nil without an observer); reports the outcome and re-raises whatever the body raised.
|
|
65
|
+
def emit_action(element, action, args = nil)
|
|
66
|
+
return yield(nil) if Raider.observer.nil? || Thread.current[GUARD_KEY]
|
|
67
|
+
|
|
68
|
+
run = Run.new(element, action, args)
|
|
69
|
+
begin
|
|
70
|
+
result = yield(run)
|
|
71
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- reports and re-raises, never swallows
|
|
72
|
+
run.finish(e)
|
|
73
|
+
raise
|
|
74
|
+
end
|
|
75
|
+
run.finish(nil)
|
|
76
|
+
result
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def started(element, action, args)
|
|
80
|
+
stamp = locator_of(element)
|
|
81
|
+
{ action: action, locator: stamp&.slice(:strategy, :selector), within: stamp ? stamp[:within] : [], args: args }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def locator_of(element)
|
|
85
|
+
element.instance_variable_get(:@raider_locator)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def stamp(element, args, parent)
|
|
89
|
+
attach(element, frozen_stamp(args, parent))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# One stamp per find, shared by every element it returned.
|
|
93
|
+
def stamp_all(elements, args, parent)
|
|
94
|
+
return elements if elements.empty?
|
|
95
|
+
|
|
96
|
+
shared = frozen_stamp(args, parent)
|
|
97
|
+
elements.each { |element| attach(element, shared) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Marks the observer as busy so an action taken inside a hook is not reported again.
|
|
101
|
+
def guarded
|
|
102
|
+
previous = Thread.current[GUARD_KEY]
|
|
103
|
+
Thread.current[GUARD_KEY] = true
|
|
104
|
+
yield
|
|
105
|
+
ensure
|
|
106
|
+
Thread.current[GUARD_KEY] = previous
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Prints one line and clears the observer, unless the observer was replaced meanwhile.
|
|
110
|
+
def stop(watcher, error)
|
|
111
|
+
$stderr.puts "[raider] observer stopped: #{summary(error)}" # rubocop:disable Style/StderrPuts -- must not be silenced by -W0
|
|
112
|
+
Raider.observer = nil if Raider.observer.equal?(watcher)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def millis(started)
|
|
116
|
+
((clock - started) * 1000).round
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def clock
|
|
120
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def attach(element, stamp)
|
|
126
|
+
return element if !element.is_a?(Selenium::WebDriver::Element) || element.frozen?
|
|
127
|
+
|
|
128
|
+
element.instance_variable_set(:@raider_locator, stamp)
|
|
129
|
+
element
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def frozen_stamp(args, parent)
|
|
133
|
+
stamp = describe(args, parent)
|
|
134
|
+
stamp[:within].each(&:freeze).freeze
|
|
135
|
+
stamp.freeze
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The class and the first line of the message; never raises, whatever #message does.
|
|
139
|
+
def summary(error)
|
|
140
|
+
line = error.message.to_s.lines.first.to_s.chomp
|
|
141
|
+
"#{error.class}: #{line[0, MESSAGE_LIMIT]}"
|
|
142
|
+
rescue StandardError, ScriptError, SystemStackError
|
|
143
|
+
error.class.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def describe(args, parent)
|
|
147
|
+
how, what = args.size == 1 && args.first.is_a?(Hash) ? args.first.first : args
|
|
148
|
+
strategy = how.to_s
|
|
149
|
+
{ strategy: -STRATEGIES.fetch(strategy, strategy).to_s, selector: -selector(strategy, what, args),
|
|
150
|
+
within: chain(parent) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def selector(strategy, what, args)
|
|
154
|
+
return role_text(args) if strategy == 'role'
|
|
155
|
+
|
|
156
|
+
(what.is_a?(Array) ? what.first : what).to_s
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def role_text(args)
|
|
160
|
+
role, options = RoleFinder.parse(args)
|
|
161
|
+
[role, *options.map { |key, value| "#{key}: #{value}" }].join(', ')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def chain(parent)
|
|
165
|
+
found = parent.is_a?(Selenium::WebDriver::Element) ? locator_of(parent) : nil
|
|
166
|
+
found ? [*found[:within], found.slice(:strategy, :selector)] : []
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'selenium-webdriver'
|
|
4
|
+
|
|
5
|
+
module Raider
|
|
6
|
+
# The in-page role engine and the query that calls it.
|
|
7
|
+
module RoleFinder
|
|
8
|
+
ENGINE = File.read(File.join(__dir__, 'js', 'role_engine.js')).freeze
|
|
9
|
+
QUERY = "if (!(window.__raider && window.__raider.role)) throw new Error('__raider not installed'); " \
|
|
10
|
+
'return window.__raider.role(arguments[0] || document, arguments[1], arguments[2])'
|
|
11
|
+
|
|
12
|
+
NATIVE_MESSAGE = 'by_role needs a web context; use accessibility_id on native screens'
|
|
13
|
+
|
|
14
|
+
# Prepended to SearchContext: sends the :role key to the engine, everything else to Selenium.
|
|
15
|
+
def find_elements(*args)
|
|
16
|
+
role, options = RoleFinder.parse(args)
|
|
17
|
+
return super unless role
|
|
18
|
+
|
|
19
|
+
raise NotSupported, NATIVE_MESSAGE unless Context.web?(bridge)
|
|
20
|
+
|
|
21
|
+
RoleQuery.run(bridge, is_a?(Selenium::WebDriver::Driver) ? nil : self, role, options)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_element(*args)
|
|
25
|
+
return super unless RoleFinder.role_args?(args)
|
|
26
|
+
|
|
27
|
+
find_elements(*args).first or
|
|
28
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, "no such element: #{Locator.describe(args)}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.role_args?(args)
|
|
32
|
+
key = args.size == 1 && args.first.is_a?(Hash) ? args.first.keys.first : args.first
|
|
33
|
+
key.to_s == 'role'
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns [role, symbol-keyed options] for a role locator, nil for any other.
|
|
37
|
+
def self.parse(args)
|
|
38
|
+
return unless role_args?(args)
|
|
39
|
+
|
|
40
|
+
value = args.size == 1 ? args.first.values.first : args[1]
|
|
41
|
+
role, options = value.is_a?(Array) ? value : [value, nil]
|
|
42
|
+
raise ArgumentError, 'role: needs a role name and an optional options Hash' unless options.nil? || options.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
options = (options || {}).transform_keys(&:to_sym)
|
|
45
|
+
raise ArgumentError, "role: name must be a String, got #{options[:name].class}" unless [NilClass, String].include?(options[:name].class)
|
|
46
|
+
|
|
47
|
+
token = validated(role)
|
|
48
|
+
RoleLocator.validated_level(token, options[:level])
|
|
49
|
+
[token, options]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.validated(role)
|
|
53
|
+
raise ArgumentError, "role: needs a role name, got #{role.inspect}" unless role.is_a?(String) || role.is_a?(Symbol)
|
|
54
|
+
|
|
55
|
+
RoleLocator.validated_role(role)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Runs a role query through the bridge, installing the engine on first use.
|
|
60
|
+
module RoleQuery
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
def run(bridge, scope, role, options)
|
|
64
|
+
options_js = options_js(options)
|
|
65
|
+
bridge.execute_script(RoleFinder::QUERY, scope, role, options_js)
|
|
66
|
+
rescue Selenium::WebDriver::Error::JavascriptError => e
|
|
67
|
+
raise unless e.message.include?('__raider')
|
|
68
|
+
|
|
69
|
+
bridge.execute_script("#{RoleFinder::ENGINE}\n#{RoleFinder::QUERY}", scope, role, options_js)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def options_js(options)
|
|
73
|
+
{
|
|
74
|
+
'name' => options[:name],
|
|
75
|
+
'exact' => options[:exact] ? true : false,
|
|
76
|
+
'includeHidden' => options[:include_hidden] ? true : false,
|
|
77
|
+
'level' => options[:level]
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Selenium::WebDriver::SearchContext.prepend(Raider::RoleFinder)
|