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,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec'
|
|
4
|
+
require_relative '../../../lib/ruby_raider/runtime/rspec'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'Raider block expectations with value matchers' do
|
|
7
|
+
include Raider::Matchers
|
|
8
|
+
|
|
9
|
+
before { Raider.timeout = 0.3 }
|
|
10
|
+
|
|
11
|
+
after { Raider.reset }
|
|
12
|
+
|
|
13
|
+
let(:counter) do
|
|
14
|
+
Class.new do
|
|
15
|
+
attr_reader :calls
|
|
16
|
+
|
|
17
|
+
def initialize = @calls = 0
|
|
18
|
+
def next_value = @calls += 1
|
|
19
|
+
end.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'retries a value matcher until the block value matches' do
|
|
23
|
+
expect { counter.next_value }.to eq(3)
|
|
24
|
+
expect(counter.calls).to eq(3)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'fails with the waited time and rspec own message' do
|
|
28
|
+
expect { expect { 1 }.to eq(2) }
|
|
29
|
+
.to raise_error(RSpec::Expectations::ExpectationNotMetError, /\(waited 0\.[34]\d?s\) \nexpected: 2\n got: 1/)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'fails a negated matcher with the waited time' do
|
|
33
|
+
expect { expect { 'x' }.not_to eq('x') }
|
|
34
|
+
.to raise_error(RSpec::Expectations::ExpectationNotMetError, /\(waited 0\.[34]\d?s\) \nexpected: value != "x"\n got: "x"/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'retries not_to include until the value stops including it' do
|
|
38
|
+
values = [%w[x y], %w[x], %w[y]]
|
|
39
|
+
expect { values.size > 1 ? values.shift : values.first }.not_to include('x')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'retries to_not as well' do
|
|
43
|
+
values = [1, 1, 2]
|
|
44
|
+
expect { values.size > 1 ? values.shift : values.first }.to_not eq(1) # rubocop:disable RSpec/NotToNot
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'retries match, be_empty and be > 3' do
|
|
48
|
+
n = 0
|
|
49
|
+
expect { (n += 1) > 1 ? 'abc' : 'x' }.to match(/abc/)
|
|
50
|
+
m = 0
|
|
51
|
+
expect { (m += 1) > 1 ? [] : [1] }.to be_empty
|
|
52
|
+
k = 0
|
|
53
|
+
expect { k += 1 }.to be > 3
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'retries through lookup errors' do
|
|
57
|
+
calls = 0
|
|
58
|
+
result = proc do
|
|
59
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, 'gone' if (calls += 1) <= 2
|
|
60
|
+
|
|
61
|
+
'ok'
|
|
62
|
+
end
|
|
63
|
+
expect(&result).to eq('ok')
|
|
64
|
+
expect(calls).to eq(3)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'retries AmbiguousMatch and stale elements' do
|
|
68
|
+
calls = 0
|
|
69
|
+
expect do
|
|
70
|
+
calls += 1
|
|
71
|
+
raise Raider::AmbiguousMatch.new('d', []) if calls == 1
|
|
72
|
+
raise Selenium::WebDriver::Error::StaleElementReferenceError, 'stale' if calls == 2
|
|
73
|
+
|
|
74
|
+
7
|
|
75
|
+
end.to eq(7)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 're-raises the last lookup error when no value was ever read' do
|
|
79
|
+
expect { expect { raise Selenium::WebDriver::Error::NoSuchElementError, 'never' }.to eq(1) }
|
|
80
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /never/)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'makes finds inside the block a single attempt' do
|
|
84
|
+
seen = nil
|
|
85
|
+
expect { seen = Raider::Wait.immediate? }.to be(true)
|
|
86
|
+
expect(seen).to be(true)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'evaluates raise_error once' do
|
|
90
|
+
calls = 0
|
|
91
|
+
expect { (calls += 1) && raise(ArgumentError) }.to raise_error(ArgumentError)
|
|
92
|
+
expect(calls).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'evaluates change once' do
|
|
96
|
+
list = []
|
|
97
|
+
calls = 0
|
|
98
|
+
expect { list << (calls += 1) }.to change { list.size }.by(1)
|
|
99
|
+
expect(calls).to eq(1)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'evaluates output once' do
|
|
103
|
+
calls = 0
|
|
104
|
+
expect { [calls += 1, print('a')] }.to output('a').to_stdout
|
|
105
|
+
expect(calls).to eq(1)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'evaluates throw_symbol once' do
|
|
109
|
+
calls = 0
|
|
110
|
+
expect { (calls += 1) && throw(:done) }.to throw_symbol(:done)
|
|
111
|
+
expect(calls).to eq(1)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'evaluates yield_control once' do
|
|
115
|
+
calls = 0
|
|
116
|
+
expect { |probe| (calls += 1) && probe.to_proc.call }.to yield_control
|
|
117
|
+
expect(calls).to eq(1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'keeps a Raider matcher on a block with its own wait and message' do
|
|
121
|
+
Raider.timeout = 2
|
|
122
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
123
|
+
expect { expect { [1] }.to have_count(2, wait: 0.2) }
|
|
124
|
+
.to raise_error(RSpec::Expectations::ExpectationNotMetError, /expected the block to have count 2, last seen 1/)
|
|
125
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'treats a timeout inside the block as not ready and re-raises it after the deadline' do
|
|
129
|
+
expect { expect { Raider::Wait.until(timeout: 0.05) { false } }.to eq(1) }
|
|
130
|
+
.to raise_error(Raider::TimeoutError, /condition not met/)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'passes once a block that raised a Raider::TimeoutError twice returns the value' do
|
|
134
|
+
calls = 0
|
|
135
|
+
expect { (calls += 1) <= 2 ? raise(Raider::TimeoutError, 'slow') : 5 }.to eq(5)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'reports the last poll error rather than an older value' do
|
|
139
|
+
calls = 0
|
|
140
|
+
block = proc do
|
|
141
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, 'later gone' if (calls += 1) > 1
|
|
142
|
+
|
|
143
|
+
'x'
|
|
144
|
+
end
|
|
145
|
+
expect { expect(&block).to eq('y') }
|
|
146
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /later gone/)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'reports the last value when the last poll read one' do
|
|
150
|
+
calls = 0
|
|
151
|
+
block = proc do
|
|
152
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, 'early' if (calls += 1) == 1
|
|
153
|
+
|
|
154
|
+
'x'
|
|
155
|
+
end
|
|
156
|
+
expect { expect(&block).to eq('y') }
|
|
157
|
+
.to raise_error(RSpec::Expectations::ExpectationNotMetError, /got: "x"/)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it 'does not chain the internal timeout as a cause' do
|
|
161
|
+
expect { expect { 1 }.to eq(2) }.to raise_error(RSpec::Expectations::ExpectationNotMetError) { |error|
|
|
162
|
+
expect(error.cause).to be_nil
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec'
|
|
4
|
+
require_relative '../../lib/ruby_raider/runtime'
|
|
5
|
+
require_relative 'support/fake_bridge'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'Raider::Observer' do
|
|
8
|
+
let(:bridge) { Raider::Spec::FakeBridge.new }
|
|
9
|
+
let(:driver) { Raider::Spec.driver(bridge) }
|
|
10
|
+
let(:recorder_class) do
|
|
11
|
+
Class.new do
|
|
12
|
+
attr_reader :events
|
|
13
|
+
|
|
14
|
+
def initialize = @events = []
|
|
15
|
+
|
|
16
|
+
def action_started(**details)
|
|
17
|
+
@events << [:action_started, details]
|
|
18
|
+
:token
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def action_ready(token, **details) = @events << [:action_ready, token, details]
|
|
22
|
+
def action_finished(token, **details) = @events << [:action_finished, token, details]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
let(:recorder) { recorder_class.new }
|
|
26
|
+
|
|
27
|
+
before do
|
|
28
|
+
Raider.reset
|
|
29
|
+
Raider::Actionability.reset_notice
|
|
30
|
+
allow($stderr).to receive(:puts)
|
|
31
|
+
allow(Raider::Describe).to receive(:element) { |element| "<el #{element.ref.last}>" }
|
|
32
|
+
Raider.observer = recorder
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
after { Raider.reset }
|
|
36
|
+
|
|
37
|
+
def names = recorder.events.map(&:first)
|
|
38
|
+
def finished = recorder.events.last.last
|
|
39
|
+
|
|
40
|
+
it 'reports a role click with its retries' do
|
|
41
|
+
bridge.script_results.push([Selenium::WebDriver::Element.new(bridge, 'a')])
|
|
42
|
+
button = driver.find_element(Raider.by_role('button', name: 'Save'))
|
|
43
|
+
bridge.script_results.push({ 'check' => 'stable', 'first' => false }, { 'check' => 'stable', 'first' => false }, nil)
|
|
44
|
+
button.click
|
|
45
|
+
started, ready, done = recorder.events
|
|
46
|
+
expect(started).to eq([:action_started, { action: :click, locator: { strategy: 'role', selector: 'button, name: Save' }, within: [], args: nil }])
|
|
47
|
+
expect(ready).to eq([:action_ready, :token, { element: button }])
|
|
48
|
+
expect(done[1]).to eq(:token)
|
|
49
|
+
expect(done[2]).to include(status: :passed, retries: 2, error: nil, element: button)
|
|
50
|
+
expect(done[2][:waited_ms]).to be_a(Integer)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'hands the observer a frozen copy of the keys so it cannot change what is typed' do
|
|
54
|
+
keys = ['a']
|
|
55
|
+
seen = nil
|
|
56
|
+
recorder.define_singleton_method(:action_started) { |**details| (seen = details[:args]) << 'evil' }
|
|
57
|
+
bridge.enqueue(['f'])
|
|
58
|
+
driver.find_element(id: 'x').send_keys(*keys)
|
|
59
|
+
expect(seen).to be_frozen
|
|
60
|
+
expect(bridge.calls.select { |call| call.first == :send_keys_to_element }.map(&:last)).to eq([['a']])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'reports a nested send_keys with its within chain and keys' do
|
|
64
|
+
bridge.enqueue(['f'])
|
|
65
|
+
form = driver.find_element(css: 'form')
|
|
66
|
+
bridge.enqueue(['i'])
|
|
67
|
+
field = form.find_element(id: 'email')
|
|
68
|
+
bridge.script_results.push(nil)
|
|
69
|
+
field.send_keys('a')
|
|
70
|
+
expect(recorder.events.first.last).to eq(
|
|
71
|
+
action: :send_keys, locator: { strategy: 'id', selector: 'email' },
|
|
72
|
+
within: [{ strategy: 'css', selector: 'form' }], args: ['a']
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it 'reports clear and submit with nil args' do
|
|
77
|
+
bridge.enqueue(['a'])
|
|
78
|
+
el = driver.find_element(name: 'q')
|
|
79
|
+
bridge.script_results.push(nil, nil)
|
|
80
|
+
el.clear
|
|
81
|
+
el.submit
|
|
82
|
+
expect(recorder.events.select { |e| e.first == :action_started }.map { |e| [e.last[:action], e.last[:args]] })
|
|
83
|
+
.to eq([[:clear, nil], [:submit, nil]])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'reports a failed timeout with the error and re-raises it' do
|
|
87
|
+
Raider.timeout = 0.3
|
|
88
|
+
bridge.enqueue(['a'])
|
|
89
|
+
el = driver.find_element(id: 'x')
|
|
90
|
+
bridge.script_results.concat([{ 'check' => 'visible' }] * 50)
|
|
91
|
+
expect { el.click }.to raise_error(Raider::NotActionable) { |error|
|
|
92
|
+
expect(finished).to include(status: :failed, error: error)
|
|
93
|
+
expect(finished[:retries]).to eq(error.retries)
|
|
94
|
+
expect(error.retries).to be >= 1
|
|
95
|
+
}
|
|
96
|
+
expect(names).to eq(%i[action_started action_finished])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 're-raises a non-StandardError after reporting it' do
|
|
100
|
+
el = Selenium::WebDriver::Element.new(bridge, 'e1')
|
|
101
|
+
bridge.script_results.push(Interrupt.new('stop'))
|
|
102
|
+
expect { el.click }.to raise_error(Interrupt)
|
|
103
|
+
expect(finished).to include(status: :failed)
|
|
104
|
+
expect(finished[:error]).to be_a(Interrupt)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'still reports with force: true' do
|
|
108
|
+
bridge.answer(:element_tag_name, 'div')
|
|
109
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click(force: true)
|
|
110
|
+
expect(names).to eq(%i[action_started action_ready action_finished])
|
|
111
|
+
expect(finished).to include(status: :passed, retries: 0)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'still reports with actionability off' do
|
|
115
|
+
Raider.actionability = false
|
|
116
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click
|
|
117
|
+
expect(names).to eq(%i[action_started action_ready action_finished])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'still reports a native click' do
|
|
121
|
+
appium = Class.new(Raider::Spec::FakeBridge) { def current_context = 'NATIVE_APP' }.new
|
|
122
|
+
appium.answer(:element_displayed?, false, true).answer(:element_enabled?, true)
|
|
123
|
+
Selenium::WebDriver::Element.new(appium, 'e1').click
|
|
124
|
+
expect(names).to eq(%i[action_started action_ready action_finished])
|
|
125
|
+
expect(finished[:retries]).to eq(1)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'gives a nil locator to an unstamped element' do
|
|
129
|
+
bridge.answer(:element_tag_name, 'div')
|
|
130
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click(force: true)
|
|
131
|
+
expect(recorder.events.first.last).to include(locator: nil, within: [])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it 'lets the click happen when the observer raises, prints once and clears it' do
|
|
135
|
+
recorder.define_singleton_method(:action_started) { |**| raise ArgumentError, 'boom' }
|
|
136
|
+
bridge.answer(:element_tag_name, 'div')
|
|
137
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click(force: true)
|
|
138
|
+
expect(bridge.calls.last).to eq([:click_element, 'e1'])
|
|
139
|
+
expect($stderr).to have_received(:puts).with('[raider] observer stopped: ArgumentError: boom').once
|
|
140
|
+
expect(Raider.observer).to be_nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'passes the token the observer returned back to later events' do
|
|
144
|
+
bridge.answer(:element_tag_name, 'div')
|
|
145
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click(force: true)
|
|
146
|
+
expect(recorder.events.map { |e| e[1] }.drop(1)).to eq(%i[token token])
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe 'a broken observer' do
|
|
150
|
+
let(:element) { Selenium::WebDriver::Element.new(bridge, 'e1') }
|
|
151
|
+
|
|
152
|
+
before { bridge.answer(:element_tag_name, 'div') }
|
|
153
|
+
|
|
154
|
+
[NotImplementedError, SystemStackError, ArgumentError].each do |klass|
|
|
155
|
+
%i[action_started action_ready action_finished].each do |hook|
|
|
156
|
+
it "survives #{klass} in #{hook}: the click happens, one line, observer cleared" do
|
|
157
|
+
recorder.define_singleton_method(hook) { |*, **| raise klass, 'boom' }
|
|
158
|
+
element.click(force: true)
|
|
159
|
+
expect(bridge.calls.last).to eq([:click_element, 'e1'])
|
|
160
|
+
expect($stderr).to have_received(:puts).with("[raider] observer stopped: #{klass}: boom").once
|
|
161
|
+
expect(Raider.observer).to be_nil
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'lets an Interrupt from the observer propagate' do
|
|
167
|
+
recorder.define_singleton_method(:action_started) { |**| raise Interrupt }
|
|
168
|
+
expect { element.click(force: true) }.to raise_error(Interrupt)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
it 'truncates the message to its first line and 200 characters' do
|
|
172
|
+
recorder.define_singleton_method(:action_started) { |**| raise ArgumentError, "#{'x' * 500}\nsecond" }
|
|
173
|
+
element.click(force: true)
|
|
174
|
+
expect($stderr).to have_received(:puts).with("[raider] observer stopped: ArgumentError: #{'x' * 200}")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it 'survives an exception whose message raises' do
|
|
178
|
+
error = Class.new(StandardError) { def message = raise('no') }
|
|
179
|
+
recorder.define_singleton_method(:action_started) { |**| raise error }
|
|
180
|
+
expect { element.click(force: true) }.not_to raise_error
|
|
181
|
+
expect(Raider.observer).to be_nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'leaves a replacement observer installed' do
|
|
185
|
+
other = recorder_class.new
|
|
186
|
+
recorder.define_singleton_method(:action_started) do |**|
|
|
187
|
+
Raider.observer = other
|
|
188
|
+
raise 'swap'
|
|
189
|
+
end
|
|
190
|
+
element.click(force: true)
|
|
191
|
+
expect(Raider.observer).to equal(other)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'does not report again for an action taken inside a hook' do
|
|
195
|
+
inner = Selenium::WebDriver::Element.new(bridge, 'e2')
|
|
196
|
+
started = 0
|
|
197
|
+
recorder.define_singleton_method(:action_started) do |**|
|
|
198
|
+
started += 1
|
|
199
|
+
inner.click(force: true)
|
|
200
|
+
:token
|
|
201
|
+
end
|
|
202
|
+
element.click(force: true)
|
|
203
|
+
expect(bridge.calls.select { |c| c.first == :click_element }).to eq([[:click_element, 'e2'], [:click_element, 'e1']])
|
|
204
|
+
expect(started).to eq(1)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it 'sends no follow-up events when action_started returns nil' do
|
|
208
|
+
events = []
|
|
209
|
+
recorder.define_singleton_method(:action_started) { |**| nil }
|
|
210
|
+
recorder.define_singleton_method(:action_ready) { |*, **| events << :ready }
|
|
211
|
+
recorder.define_singleton_method(:action_finished) { |*, **| events << :finished }
|
|
212
|
+
element.click(force: true)
|
|
213
|
+
expect(events).to be_empty
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
describe 'stamping' do
|
|
218
|
+
it 'shares one frozen stamp across a find_elements result' do
|
|
219
|
+
bridge.enqueue(%w[a b c])
|
|
220
|
+
stamps = driver.find_elements(id: 'x').map { |e| e.instance_variable_get(:@raider_locator) }
|
|
221
|
+
expect(stamps.map(&:object_id).uniq.size).to eq(1)
|
|
222
|
+
expect([stamps.first.frozen?, stamps.first[:within].frozen?]).to eq([true, true])
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it 'deep-freezes the stamp and keeps the caller string out of it' do
|
|
226
|
+
bridge.enqueue(['a'])
|
|
227
|
+
selector = +'#login'
|
|
228
|
+
stamp = driver.find_element(css: selector).instance_variable_get(:@raider_locator)
|
|
229
|
+
expect([stamp[:strategy].frozen?, stamp[:selector].frozen?]).to eq([true, true])
|
|
230
|
+
expect(stamp[:selector]).not_to equal(selector)
|
|
231
|
+
selector << '-changed'
|
|
232
|
+
expect(stamp[:selector]).to eq('#login')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'leaves a frozen element unstamped without raising' do
|
|
236
|
+
element = Selenium::WebDriver::Element.new(bridge, 'a').freeze
|
|
237
|
+
expect(Raider::Observer.stamp(element, [{ id: 'x' }], driver)).to equal(element)
|
|
238
|
+
expect(Raider::Observer.locator_of(element)).to be_nil
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it 'chains three levels, outermost first' do
|
|
242
|
+
%w[m f i].each { |id| bridge.enqueue([id]) }
|
|
243
|
+
field = driver.find_element(css: 'main').find_element(css: 'form').find_element(id: 'email')
|
|
244
|
+
expect(Raider::Observer.locator_of(field)[:within]).to eq(
|
|
245
|
+
[{ strategy: 'css', selector: 'main' }, { strategy: 'css', selector: 'form' }]
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
it 'stamps through the first alias' do
|
|
250
|
+
bridge.enqueue(['a'])
|
|
251
|
+
expect(Raider::Observer.locator_of(driver.first(id: 'x'))).to include(strategy: 'id', selector: 'x')
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it 'takes the first item of an Array-form selector through a real find' do
|
|
255
|
+
bridge.enqueue(['a'])
|
|
256
|
+
found = driver.find_element(text: ['Save', { exact: true }])
|
|
257
|
+
expect(Raider::Observer.locator_of(found)).to include(strategy: 'text', selector: 'Save')
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
describe 'Raider.report' do
|
|
262
|
+
it 'returns nil without an observer or when the hook is missing' do
|
|
263
|
+
Raider.observer = Object.new
|
|
264
|
+
expect(Raider.report(:nothing)).to be_nil
|
|
265
|
+
Raider.observer = nil
|
|
266
|
+
expect(Raider.report(:nothing) { raise 'not built' }).to be_nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'builds the details from the block only for an observer that responds' do
|
|
270
|
+
seen = nil
|
|
271
|
+
Raider.observer = Object.new.tap { |o| o.define_singleton_method(:hook) { |first, **d| seen = [first, d] } }
|
|
272
|
+
Raider.report(:hook, 1) { { a: 2 } }
|
|
273
|
+
expect(seen).to eq([1, { a: 2 }])
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
it 'catches a failing block under the same rescue' do
|
|
277
|
+
Raider.observer = recorder
|
|
278
|
+
Raider.report(:action_started) { raise 'bad' }
|
|
279
|
+
expect($stderr).to have_received(:puts).with('[raider] observer stopped: RuntimeError: bad')
|
|
280
|
+
expect(Raider.observer).to be_nil
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe 'with no observer' do
|
|
285
|
+
before { Raider.observer = nil }
|
|
286
|
+
|
|
287
|
+
it 'sends the same bridge commands as a bare click' do
|
|
288
|
+
bridge.script_results.push({ 'check' => 'visible' }, nil)
|
|
289
|
+
Selenium::WebDriver::Element.new(bridge, 'e1').click
|
|
290
|
+
expect(bridge.calls).to eq([[:click_element, 'e1']])
|
|
291
|
+
expect(bridge.script_calls.size).to eq(2)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'yields without a run' do
|
|
295
|
+
expect { |block| Raider::Observer.emit_action(nil, :click, &block) }.to yield_with_args(nil)
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
describe '.locator_of' do
|
|
300
|
+
# Straight through the stamp: Selenium rejects some of these spellings before any find succeeds.
|
|
301
|
+
def locate(*args)
|
|
302
|
+
element = Selenium::WebDriver::Element.new(bridge, 'a')
|
|
303
|
+
Raider::Observer.locator_of(Raider::Observer.stamp(element, args, driver))
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
{
|
|
307
|
+
[:css_selector, 'a'] => %w[css a], [{ class: 'k' }] => %w[class_name k], [{ link: 'l' }] => %w[link_text l],
|
|
308
|
+
[:partial_link, 'p'] => %w[partial_link_text p], ['class name', 'k'] => %w[class_name k],
|
|
309
|
+
['css selector', 'a'] => %w[css a], ['link text', 'l'] => %w[link_text l],
|
|
310
|
+
['partial link text', 'p'] => %w[partial_link_text p], ['tag name', 'div'] => %w[tag_name div],
|
|
311
|
+
[:id, 'x'] => %w[id x], [{ xpath: '//a' }] => ['xpath', '//a'], [{ label: 'Email' }] => %w[label Email],
|
|
312
|
+
[{ test_id: 't' }] => %w[test_id t], [{ text: ['Hi', { exact: true }] }] => %w[text Hi]
|
|
313
|
+
}.each do |args, (strategy, selector)|
|
|
314
|
+
it "normalises #{args.inspect}" do
|
|
315
|
+
expect(locate(*args)).to eq(strategy: strategy, selector: selector, within: [])
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
it 'renders a role with its options' do
|
|
320
|
+
expect(locate(role: ['button', { name: 'Save', exact: true }])[:selector]).to eq('button, name: Save, exact: true')
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it 'renders a bare role' do
|
|
324
|
+
expect(locate(role: 'button')).to include(strategy: 'role', selector: 'button')
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
it 'stamps find_elements results too' do
|
|
328
|
+
bridge.enqueue(%w[a b])
|
|
329
|
+
found = driver.find_elements(id: 'x')
|
|
330
|
+
expect(found.map { |e| Raider::Observer.locator_of(e) }).to all(include(strategy: 'id', selector: 'x'))
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
it 'returns nil for an unstamped element' do
|
|
334
|
+
expect(Raider::Observer.locator_of(Selenium::WebDriver::Element.new(bridge, 'e1'))).to be_nil
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
it 'names the runtime directory as LIB_DIR' do
|
|
339
|
+
expect(Raider::Observer::LIB_DIR).to eq(File.expand_path('../../lib/ruby_raider/runtime', __dir__))
|
|
340
|
+
end
|
|
341
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec'
|
|
4
|
+
require_relative '../../lib/ruby_raider/runtime'
|
|
5
|
+
require_relative 'support/fake_bridge'
|
|
6
|
+
|
|
7
|
+
RSpec.describe 'Raider role finder' do
|
|
8
|
+
let(:bridge) { Raider::Spec::FakeBridge.new }
|
|
9
|
+
let(:driver) { Raider::Spec.driver(bridge) }
|
|
10
|
+
let(:locator) { Raider.by_role('button', name: 'Save') }
|
|
11
|
+
let(:options) { { 'name' => 'Save', 'exact' => false, 'includeHidden' => false, 'level' => nil } }
|
|
12
|
+
|
|
13
|
+
before { allow(Raider::Describe).to receive(:element) { |element| "<el #{element.ref.last}>" } }
|
|
14
|
+
|
|
15
|
+
after { Raider.reset }
|
|
16
|
+
|
|
17
|
+
# These examples are about the role engine, so each find makes one attempt.
|
|
18
|
+
around { |example| Raider::Wait.immediately { example.run } }
|
|
19
|
+
|
|
20
|
+
def element(id)
|
|
21
|
+
Selenium::WebDriver::Element.new(bridge, id)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'queries the engine with a nil scope from the driver' do
|
|
25
|
+
bridge.script_results << [element('a')]
|
|
26
|
+
driver.find_elements(locator)
|
|
27
|
+
expect(bridge.script_calls.first.drop(1)).to eq([nil, 'button', options])
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'scopes the query to the element' do
|
|
31
|
+
scope = element('s')
|
|
32
|
+
bridge.script_results << []
|
|
33
|
+
scope.find_elements(Raider.by_role('listitem'))
|
|
34
|
+
expect(bridge.script_calls.first.drop(1)).to eq([scope, 'listitem', { 'name' => nil, 'exact' => false,
|
|
35
|
+
'includeHidden' => false, 'level' => nil }])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'scopes the query to a shadow root instead of the document' do
|
|
39
|
+
shadow = Selenium::WebDriver::ShadowRoot.new(bridge, 'sr')
|
|
40
|
+
bridge.script_results << []
|
|
41
|
+
shadow.find_elements(Raider.by_role('button'))
|
|
42
|
+
expect(bridge.script_calls.first[1]).to equal(shadow)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'carries a heading level and rejects bad ones before any script runs' do
|
|
46
|
+
bridge.script_results << []
|
|
47
|
+
driver.find_elements(Raider.by_role('heading', level: 2))
|
|
48
|
+
expect(bridge.script_calls.first.last).to include('level' => 2)
|
|
49
|
+
[{ role: ['button', { level: 2 }] }, { role: ['heading', { level: 7 }] }, { role: ['heading', { level: '2' }] }].each do |bad|
|
|
50
|
+
expect { driver.find_elements(bad) }.to raise_error(ArgumentError, /level/)
|
|
51
|
+
end
|
|
52
|
+
expect(bridge.script_calls.size).to eq(1)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'accepts the two-argument form and string option keys' do
|
|
56
|
+
bridge.script_results << []
|
|
57
|
+
driver.find_elements(:role, ['button', { 'name' => 'Save' }])
|
|
58
|
+
expect(bridge.script_calls.first.drop(1)).to eq([nil, 'button', options])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'accepts a bare role string' do
|
|
62
|
+
bridge.script_results << []
|
|
63
|
+
driver.find_elements(role: 'button')
|
|
64
|
+
expect(bridge.script_calls.first[2]).to eq('button')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it 'accepts a string role key in both forms' do
|
|
68
|
+
bridge.script_results.push([], [])
|
|
69
|
+
driver.find_elements('role' => ['button', { name: 'Save' }])
|
|
70
|
+
driver.find_elements('role', ['button', { name: 'Save' }])
|
|
71
|
+
expect(bridge.script_calls.map { |call| call.drop(1) }).to eq([[nil, 'button', options]] * 2)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'finds a single element through a string role key' do
|
|
75
|
+
bridge.script_results << [element('a')]
|
|
76
|
+
expect(driver.find_element('role' => 'button').ref.last).to eq('a')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
'a nil value' => { role: nil },
|
|
81
|
+
'an unknown role' => { role: 'buton' },
|
|
82
|
+
'a Hash value' => { role: { a: 1 } },
|
|
83
|
+
'a blank role' => { role: ['', { name: 'x' }] },
|
|
84
|
+
'non-Hash options' => { role: %w[button Save] },
|
|
85
|
+
'a Regexp name' => { role: ['button', { name: /Save/ }] }
|
|
86
|
+
}.each do |label, locator|
|
|
87
|
+
it "raises ArgumentError for #{label} before any script runs" do
|
|
88
|
+
expect { driver.find_elements(locator) }.to raise_error(ArgumentError, /role|unknown role/)
|
|
89
|
+
expect { driver.find_element(locator) }.to raise_error(ArgumentError)
|
|
90
|
+
expect(bridge.script_calls).to be_empty
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'raises AmbiguousMatch from find_element on two matches' do
|
|
95
|
+
bridge.script_results << [element('a'), element('b')]
|
|
96
|
+
expect { driver.find_element(locator) }.to raise_error(Raider::AmbiguousMatch) { |error|
|
|
97
|
+
expect(error.message).to start_with('`by_role(\'button\', name: "Save")` matched 2 elements')
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it 'returns the single match' do
|
|
102
|
+
bridge.script_results << [element('a')]
|
|
103
|
+
expect(driver.find_element(locator).ref.last).to eq('a')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'returns the first match when strict is off' do
|
|
107
|
+
Raider.strict = false
|
|
108
|
+
bridge.script_results << [element('a'), element('b')]
|
|
109
|
+
expect(driver.find_element(locator).ref.last).to eq('a')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'raises NoSuchElementError with no matches' do
|
|
113
|
+
bridge.script_results << []
|
|
114
|
+
expect { driver.find_element(locator) }
|
|
115
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /no such element: by_role\('button', name: "Save"\)/)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'raises NoSuchElementError with no matches when strict is off' do
|
|
119
|
+
Raider.strict = false
|
|
120
|
+
bridge.script_results << []
|
|
121
|
+
expect { driver.find_element(locator) }
|
|
122
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /no such element: by_role/)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'propagates a JavascriptError that is not about the engine' do
|
|
126
|
+
bridge.script_results << Selenium::WebDriver::Error::JavascriptError.new('boom')
|
|
127
|
+
expect { driver.find_elements(locator) }.to raise_error(Selenium::WebDriver::Error::JavascriptError, 'boom')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'leaves other locators for Selenium' do
|
|
131
|
+
bridge.enqueue(%w[a b])
|
|
132
|
+
expect(driver.find_elements(css: 'x').size).to eq(2)
|
|
133
|
+
expect(bridge.calls.first.first).to eq(:find_elements_by)
|
|
134
|
+
expect(bridge.script_calls).to be_empty
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'with an Appium NATIVE_APP bridge' do
|
|
138
|
+
let(:bridge) do
|
|
139
|
+
klass = stub_const('Appium::Core::Base::Bridge', Class.new(Raider::Spec::FakeBridge) do
|
|
140
|
+
class << self; attr_accessor :locator_converter; end
|
|
141
|
+
define_method(:current_context) { 'NATIVE_APP' }
|
|
142
|
+
end)
|
|
143
|
+
klass.locator_converter = Selenium::WebDriver::Remote::Bridge::LocatorConverter.new
|
|
144
|
+
klass.new
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it 'raises NotSupported' do
|
|
148
|
+
expect { driver.find_elements(locator) }
|
|
149
|
+
.to raise_error(Raider::NotSupported, 'by_role needs a web context; use accessibility_id on native screens')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'raises NotSupported from find_element too' do
|
|
153
|
+
expect { driver.find_element(locator) }.to raise_error(Raider::NotSupported)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|