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,75 @@
|
|
|
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::RoleQuery' do
|
|
8
|
+
let(:bridge) { Raider::Spec::FakeBridge.new }
|
|
9
|
+
let(:elements) { %w[a b] }
|
|
10
|
+
let(:missing) { Selenium::WebDriver::Error::JavascriptError.new('ReferenceError: window.__raider is undefined') }
|
|
11
|
+
|
|
12
|
+
it 'queries once when the engine is already installed' do
|
|
13
|
+
bridge.script_results << elements
|
|
14
|
+
expect(Raider::RoleQuery.run(bridge, nil, 'button', {})).to eq(elements)
|
|
15
|
+
expect(bridge.script_calls.length).to eq(1)
|
|
16
|
+
expect(bridge.script_calls.first.first).to eq(Raider::RoleFinder::QUERY)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'installs the engine once and re-runs the query when it is missing' do
|
|
20
|
+
bridge.script_results.push(missing, elements)
|
|
21
|
+
expect(Raider::RoleQuery.run(bridge, nil, 'button', {})).to eq(elements)
|
|
22
|
+
expect(bridge.script_calls.length).to eq(2)
|
|
23
|
+
expect(bridge.script_calls.last.first).to start_with(Raider::RoleFinder::ENGINE)
|
|
24
|
+
expect(bridge.script_calls.last.first).to end_with(Raider::RoleFinder::QUERY)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'installs the engine on the Chromium-style missing-engine error' do
|
|
28
|
+
bridge.script_results.push(Selenium::WebDriver::Error::JavascriptError.new('javascript error: __raider not installed'),
|
|
29
|
+
elements)
|
|
30
|
+
expect(Raider::RoleQuery.run(bridge, nil, 'button', {})).to eq(elements)
|
|
31
|
+
expect(bridge.script_calls.length).to eq(2)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'guards the query so a missing engine throws a fixed message' do
|
|
35
|
+
expect(Raider::RoleFinder::QUERY).to include('__raider not installed')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not retry beyond the install run' do
|
|
39
|
+
bridge.script_results.push(missing, missing)
|
|
40
|
+
expect { Raider::RoleQuery.run(bridge, nil, 'button', {}) }
|
|
41
|
+
.to raise_error(Selenium::WebDriver::Error::JavascriptError)
|
|
42
|
+
expect(bridge.script_calls.length).to eq(2)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'propagates an unrelated JavascriptError' do
|
|
46
|
+
bridge.script_results << Selenium::WebDriver::Error::JavascriptError.new('TypeError: boom')
|
|
47
|
+
expect { Raider::RoleQuery.run(bridge, nil, 'button', {}) }
|
|
48
|
+
.to raise_error(Selenium::WebDriver::Error::JavascriptError, /boom/)
|
|
49
|
+
expect(bridge.script_calls.length).to eq(1)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'passes the scope, role and camelCase options' do
|
|
53
|
+
bridge.script_results << elements
|
|
54
|
+
Raider::RoleQuery.run(bridge, :scope, 'link', name: 'Save', exact: true, include_hidden: true)
|
|
55
|
+
expect(bridge.script_calls.first.drop(1))
|
|
56
|
+
.to eq([:scope, 'link', { 'name' => 'Save', 'exact' => true, 'includeHidden' => true, 'level' => nil }])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'always carries all four option keys' do
|
|
60
|
+
bridge.script_results << elements
|
|
61
|
+
Raider::RoleQuery.run(bridge, nil, 'link', {})
|
|
62
|
+
expect(bridge.script_calls.first.last).to eq('name' => nil, 'exact' => false, 'includeHidden' => false,
|
|
63
|
+
'level' => nil)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'carries the heading level' do
|
|
67
|
+
bridge.script_results << elements
|
|
68
|
+
Raider::RoleQuery.run(bridge, nil, 'heading', level: 3)
|
|
69
|
+
expect(bridge.script_calls.first.last).to include('level' => 3)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'exposes the engine source as a frozen string' do
|
|
73
|
+
expect(Raider::RoleFinder::ENGINE).to be_frozen.and include('__raider')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec'
|
|
4
|
+
require_relative '../../lib/ruby_raider/runtime/selectors'
|
|
5
|
+
|
|
6
|
+
RSpec.describe 'Raider::Selectors' do
|
|
7
|
+
let(:controls) do
|
|
8
|
+
'(self::input or self::textarea or self::select or self::button or ' \
|
|
9
|
+
"self::meter or self::output or self::progress) and not(self::input[@type='hidden'])"
|
|
10
|
+
end
|
|
11
|
+
let(:excluded) do
|
|
12
|
+
'not(self::script or self::style or self::noscript or self::template or self::title or self::head) ' \
|
|
13
|
+
'and not(ancestor::head)'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '.xpath_literal' do
|
|
17
|
+
it 'single-quotes a value without single quotes' do
|
|
18
|
+
expect(Raider::Selectors.xpath_literal('Sign in')).to eq("'Sign in'")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'double-quotes a value with only single quotes' do
|
|
22
|
+
expect(Raider::Selectors.xpath_literal("O'Neil")).to eq(%q("O'Neil"))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'single-quotes a value with only double quotes' do
|
|
26
|
+
expect(Raider::Selectors.xpath_literal('say "hi"')).to eq(%q('say "hi"'))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'uses concat when both quote kinds appear' do
|
|
30
|
+
expect(Raider::Selectors.xpath_literal(%q(O'Neil "the" boss)))
|
|
31
|
+
.to eq(%q(concat('O', "'", 'Neil "the" boss')))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'handles leading, trailing and repeated single quotes with concat' do
|
|
35
|
+
expect(Raider::Selectors.xpath_literal(%q('a''"b')))
|
|
36
|
+
.to eq(%q(concat('', "'", 'a', "'", '', "'", '"b', "'", '')))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'converts non-strings with to_s' do
|
|
40
|
+
expect(Raider::Selectors.xpath_literal(42)).to eq("'42'")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
describe '.label' do
|
|
45
|
+
it 'builds the contains union by default' do
|
|
46
|
+
expect(Raider::Selectors.label('Email', exact: false)).to eq(
|
|
47
|
+
".//*[@id = //label[contains(normalize-space(.), 'Email')]/@for] | " \
|
|
48
|
+
".//label[contains(normalize-space(.), 'Email')]//*[#{controls}] | " \
|
|
49
|
+
".//*[contains(normalize-space(@aria-label), 'Email')] | " \
|
|
50
|
+
".//*[@aria-labelledby and contains(normalize-space(string(id(@aria-labelledby))), 'Email')]"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'builds the equality union with exact: true' do
|
|
55
|
+
expect(Raider::Selectors.label('Email', exact: true)).to eq(
|
|
56
|
+
".//*[@id = //label[normalize-space(.) = 'Email']/@for] | " \
|
|
57
|
+
".//label[normalize-space(.) = 'Email']//*[#{controls}] | " \
|
|
58
|
+
".//*[normalize-space(@aria-label) = 'Email'] | " \
|
|
59
|
+
".//*[@aria-labelledby and normalize-space(string(id(@aria-labelledby))) = 'Email']"
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'does not let a quote break out of the literal' do
|
|
64
|
+
xpath = Raider::Selectors.label("x'] | //*['", exact: false)
|
|
65
|
+
expect(xpath).to include(%q("x'] | //*['"))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe '.text' do
|
|
70
|
+
it 'builds the innermost-match xpath' do
|
|
71
|
+
expect(Raider::Selectors.text('Sign in', exact: false)).to eq(
|
|
72
|
+
".//*[#{excluded} and contains(normalize-space(.), 'Sign in') and " \
|
|
73
|
+
"not(.//*[#{excluded} and contains(normalize-space(.), 'Sign in')])]"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'uses equality with exact: true' do
|
|
78
|
+
expect(Raider::Selectors.text('Sign in', exact: true)).to eq(
|
|
79
|
+
".//*[#{excluded} and normalize-space(.) = 'Sign in' and " \
|
|
80
|
+
"not(.//*[#{excluded} and normalize-space(.) = 'Sign in'])]"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'uses concat for a value with both quote kinds' do
|
|
85
|
+
literal = %q(concat('O', "'", 'Neil "the" boss'))
|
|
86
|
+
expect(Raider::Selectors.text(%q(O'Neil "the" boss), exact: false)).to eq(
|
|
87
|
+
".//*[#{excluded} and contains(normalize-space(.), #{literal}) and " \
|
|
88
|
+
"not(.//*[#{excluded} and contains(normalize-space(.), #{literal})])]"
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'single-quotes a value with only double quotes' do
|
|
93
|
+
expect(Raider::Selectors.text('say "hi"', exact: true)).to include(%q(= 'say "hi"'))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe '.attribute_css' do
|
|
98
|
+
it 'escapes the inner quote for a contains match' do
|
|
99
|
+
expect(Raider::Selectors.attribute_css('placeholder', 'a[b]"c', exact: false))
|
|
100
|
+
.to eq('[placeholder*="a[b]\"c"]')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it 'uses equality when exact' do
|
|
104
|
+
expect(Raider::Selectors.attribute_css('title', 'Hi', exact: true)).to eq('[title="Hi"]')
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'escapes backslashes before quotes' do
|
|
108
|
+
expect(Raider::Selectors.attribute_css('alt', 'a\\"b', exact: true))
|
|
109
|
+
.to eq('[alt="a\\\\\\"b"]')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it 'rejects an invalid attribute name' do
|
|
113
|
+
expect { Raider::Selectors.attribute_css('a]b', 'x', exact: true) }
|
|
114
|
+
.to raise_error(ArgumentError, 'invalid attribute name: a]b')
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe '.test_id' do
|
|
119
|
+
after { Raider.reset }
|
|
120
|
+
|
|
121
|
+
it 'uses the default attribute' do
|
|
122
|
+
expect(Raider::Selectors.test_id('x')).to eq('[data-testid="x"]')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'uses a configured attribute' do
|
|
126
|
+
Raider.test_id_attribute = 'data-qa'
|
|
127
|
+
expect(Raider::Selectors.test_id('x')).to eq('[data-qa="x"]')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'rejects a configured attribute that is not a plain name' do
|
|
131
|
+
Raider.test_id_attribute = 'data-qa]'
|
|
132
|
+
expect { Raider::Selectors.test_id('x') }.to raise_error(ArgumentError, /invalid attribute name/)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
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 strict find_element' do
|
|
8
|
+
let(:bridge) { Raider::Spec::FakeBridge.new }
|
|
9
|
+
let(:driver) { Raider::Spec.driver(bridge) }
|
|
10
|
+
|
|
11
|
+
before { allow(Raider::Describe).to receive(:element) { |element| "<el #{element.ref.last}>" } }
|
|
12
|
+
|
|
13
|
+
after { Raider.reset }
|
|
14
|
+
|
|
15
|
+
it 'raises AmbiguousMatch listing both matches' do
|
|
16
|
+
bridge.enqueue(%w[a b])
|
|
17
|
+
expect { driver.find_element(label: 'Email') }.to raise_error(Raider::AmbiguousMatch) { |error|
|
|
18
|
+
expect(error.message).to include('label: "Email"')
|
|
19
|
+
expect(error.message).to include('matched 2 elements', '<el a>', '<el b>')
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns the single match through one find_elements_by call' do
|
|
24
|
+
bridge.enqueue(['a'])
|
|
25
|
+
expect(driver.find_element(label: 'Email')).to be_a(Selenium::WebDriver::Element)
|
|
26
|
+
expect(bridge.calls.map(&:first)).to eq([:find_elements_by])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'raises NoSuchElementError when nothing matches' do
|
|
30
|
+
Raider.timeout = 0
|
|
31
|
+
expect { driver.find_element(label: 'Email') }
|
|
32
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /no such element: \{ label: "Email" \}/)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'goes back to find_element_by when strict is off' do
|
|
36
|
+
Raider.strict = false
|
|
37
|
+
bridge.enqueue(['a'])
|
|
38
|
+
driver.find_element(label: 'Email')
|
|
39
|
+
expect(bridge.calls.map(&:first)).to eq([:find_element_by])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'passes the element ref through for an element scope' do
|
|
43
|
+
bridge.enqueue(['a'])
|
|
44
|
+
Selenium::WebDriver::Element.new(bridge, 'root').find_element(label: 'Email')
|
|
45
|
+
expect(bridge.calls.last).to end_with([:element, 'root'])
|
|
46
|
+
expect(bridge.calls.last.first).to eq(:find_elements_by)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'makes driver.first strict' do
|
|
50
|
+
bridge.enqueue(%w[a b])
|
|
51
|
+
expect { driver.first(label: 'Email') }.to raise_error(Raider::AmbiguousMatch)
|
|
52
|
+
expect(bridge.calls.map(&:first)).to eq([:find_elements_by])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'makes element.first strict' do
|
|
56
|
+
bridge.enqueue(%w[a b])
|
|
57
|
+
expect { Selenium::WebDriver::Element.new(bridge, 'root').first(label: 'Email') }.to raise_error(Raider::AmbiguousMatch)
|
|
58
|
+
expect(bridge.calls.map(&:first)).to eq([:find_elements_by])
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'sends driver.first back to find_element_by when strict is off' do
|
|
62
|
+
Raider.strict = false
|
|
63
|
+
bridge.enqueue(['a'])
|
|
64
|
+
driver.first(label: 'Email')
|
|
65
|
+
expect(bridge.calls.map(&:first)).to eq([:find_element_by])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'returns find_elements results that already match without waiting' do
|
|
69
|
+
bridge.enqueue(%w[a b])
|
|
70
|
+
expect(driver.find_elements(label: 'Email').size).to eq(2)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe 'waiting for a match' do
|
|
74
|
+
def find_calls(command = :find_elements_by)
|
|
75
|
+
bridge.calls.count { |call| call.first == command }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'returns the element once it shows up, after three calls' do
|
|
79
|
+
bridge.enqueue([])
|
|
80
|
+
bridge.enqueue([])
|
|
81
|
+
bridge.enqueue(['a'])
|
|
82
|
+
expect(driver.find_element(id: 'x')).to be_a(Selenium::WebDriver::Element)
|
|
83
|
+
expect(find_calls).to eq(3)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'raises NoSuchElementError naming the wait when nothing ever matches' do
|
|
87
|
+
Raider.timeout = 0.3
|
|
88
|
+
expect { driver.find_element(id: 'x') }
|
|
89
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /no such element: \{ id: "x" \} \(waited 0\.[34]\d?s\)/)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'waits the same way when strict is off' do
|
|
93
|
+
Raider.strict = false
|
|
94
|
+
Raider.timeout = 0.3
|
|
95
|
+
expect { driver.find_element(id: 'x') }
|
|
96
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /\(waited 0\.[34]\d?s\)/)
|
|
97
|
+
expect(find_calls(:find_element_by)).to be >= 3
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'raises AmbiguousMatch on the first call' do
|
|
101
|
+
bridge.enqueue(%w[a b])
|
|
102
|
+
expect { driver.find_element(id: 'x') }.to raise_error(Raider::AmbiguousMatch)
|
|
103
|
+
expect(find_calls).to eq(1)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it 'returns the elements of find_elements once they show up, after three calls' do
|
|
107
|
+
bridge.enqueue([])
|
|
108
|
+
bridge.enqueue([])
|
|
109
|
+
bridge.enqueue(%w[a b])
|
|
110
|
+
expect(driver.find_elements(css: '.row').size).to eq(2)
|
|
111
|
+
expect(find_calls).to eq(3)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'returns [] from find_elements after about the timeout' do
|
|
115
|
+
Raider.timeout = 0.3
|
|
116
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
117
|
+
expect(driver.find_elements(css: '.row')).to eq([])
|
|
118
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be_between(0.25, 0.6)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'makes one call per find inside Wait.immediately' do
|
|
122
|
+
Raider.timeout = 5
|
|
123
|
+
Raider::Wait.immediately do
|
|
124
|
+
expect(driver.find_elements(css: '.row')).to eq([])
|
|
125
|
+
expect { driver.find_element(id: 'x') }.to raise_error(Selenium::WebDriver::Error::NoSuchElementError)
|
|
126
|
+
end
|
|
127
|
+
expect(bridge.calls.map(&:first)).to eq(%i[find_elements_by find_elements_by])
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it 'makes one call per find on the native path inside Wait.immediately' do
|
|
131
|
+
Raider.strict = false
|
|
132
|
+
Raider::Wait.immediately do
|
|
133
|
+
expect { driver.find_element(id: 'x') }.to raise_error(Selenium::WebDriver::Error::NoSuchElementError)
|
|
134
|
+
end
|
|
135
|
+
expect(bridge.calls.map(&:first)).to eq([:find_element_by])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
context 'with an Appium bridge' do
|
|
140
|
+
let(:context_name) { 'NATIVE_APP' }
|
|
141
|
+
let(:bridge) do
|
|
142
|
+
name = context_name
|
|
143
|
+
klass = stub_const('Appium::Core::Base::Bridge', Class.new(Raider::Spec::FakeBridge) do
|
|
144
|
+
class << self; attr_accessor :locator_converter; end
|
|
145
|
+
define_method(:current_context) { name }
|
|
146
|
+
end)
|
|
147
|
+
klass.locator_converter = Selenium::WebDriver::Remote::Bridge::LocatorConverter.new
|
|
148
|
+
klass.new
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'does not apply strictness on NATIVE_APP' do
|
|
152
|
+
bridge.enqueue(%w[a b])
|
|
153
|
+
driver.find_element(id: 'x')
|
|
154
|
+
expect(bridge.calls.map(&:first)).to eq([:find_element_by])
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'waits for a match on NATIVE_APP too' do
|
|
158
|
+
Raider.timeout = 0.3
|
|
159
|
+
bridge.enqueue([])
|
|
160
|
+
bridge.enqueue(['a'])
|
|
161
|
+
expect(driver.find_element(id: 'x')).to be_a(Selenium::WebDriver::Element)
|
|
162
|
+
expect { driver.find_element(id: 'gone') }
|
|
163
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /\(waited 0\.[34]\d?s\)/)
|
|
164
|
+
expect(driver.find_elements(id: 'gone')).to eq([])
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'reports the elapsed wait, not the timeout, inside a shorter enclosing wait' do
|
|
168
|
+
Raider.timeout = 5
|
|
169
|
+
expect { Raider::Wait.until(timeout: 0.2) { driver.find_element(id: 'x') } }
|
|
170
|
+
.to raise_error(Selenium::WebDriver::Error::NoSuchElementError, /\(waited 0\.[23]\d?s\)/)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it 'raises NotSupported at once for a role find on NATIVE_APP' do
|
|
174
|
+
Raider.timeout = 5
|
|
175
|
+
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
176
|
+
expect { driver.find_element(role: 'button') }.to raise_error(Raider::NotSupported)
|
|
177
|
+
expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started).to be < 1
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
context 'in a webview' do
|
|
181
|
+
let(:context_name) { 'WEBVIEW_1' }
|
|
182
|
+
|
|
183
|
+
it 'applies strictness' do
|
|
184
|
+
bridge.enqueue(%w[a b])
|
|
185
|
+
expect { driver.find_element(id: 'x') }.to raise_error(Raider::AmbiguousMatch)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'asks for the context once on a strict role find' do
|
|
189
|
+
bridge.script_results << ['a']
|
|
190
|
+
allow(bridge).to receive(:current_context).and_call_original
|
|
191
|
+
driver.find_element(role: 'button')
|
|
192
|
+
expect(bridge).to have_received(:current_context).once
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it 'asks for the context once on a strict role find that waits several polls' do
|
|
196
|
+
bridge.script_results.push([], [], ['a'])
|
|
197
|
+
allow(bridge).to receive(:current_context).and_call_original
|
|
198
|
+
driver.find_element(role: 'button')
|
|
199
|
+
expect(bridge).to have_received(:current_context).once
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it 'asks for the context once on a non-strict role find that waits several polls' do
|
|
203
|
+
Raider.strict = false
|
|
204
|
+
bridge.script_results.push([], [], ['a'])
|
|
205
|
+
allow(bridge).to receive(:current_context).and_call_original
|
|
206
|
+
driver.find_element(role: 'button')
|
|
207
|
+
expect(bridge).to have_received(:current_context).once
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it 'asks for the context once on a role find_elements that waits several polls' do
|
|
211
|
+
bridge.script_results.push([], [], ['a'])
|
|
212
|
+
allow(bridge).to receive(:current_context).and_call_original
|
|
213
|
+
expect(driver.find_elements(role: 'button').size).to eq(1)
|
|
214
|
+
expect(bridge).to have_received(:current_context).once
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it 'asks for the context once on a strict css find' do
|
|
218
|
+
bridge.enqueue(['a'])
|
|
219
|
+
allow(bridge).to receive(:current_context).and_call_original
|
|
220
|
+
driver.find_element(css: 'x')
|
|
221
|
+
expect(bridge).to have_received(:current_context).once
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
describe Raider::Locator do
|
|
227
|
+
it 'describes a role with options as by_role' do
|
|
228
|
+
expect(described_class.describe([{ role: ['button', { name: 'Save' }] }])).to eq("by_role('button', name: \"Save\")")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'shows the heading level' do
|
|
232
|
+
expect(described_class.describe([Raider.by_role('heading', level: 2)])).to eq("by_role('heading', level: 2)")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
it 'describes a role without options' do
|
|
236
|
+
expect(described_class.describe([{ role: ['button', {}] }])).to eq("by_role('button')")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'describes a plain key' do
|
|
240
|
+
expect(described_class.describe([{ label: 'Email' }])).to eq('{ label: "Email" }')
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it 'shows exact when set' do
|
|
244
|
+
expect(described_class.describe([{ label: ['Email', { exact: true }] }])).to eq('{ label: "Email", exact: true }')
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'describes the two-argument form' do
|
|
248
|
+
expect(described_class.describe(['css selector', '.a'])).to eq('{ "css selector": ".a" }')
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
it 'never raises on odd shapes' do
|
|
252
|
+
[[], [nil], [{ role: [] }], [{ role: 5 }], [1, 2, 3]].each do |args|
|
|
253
|
+
expect(described_class.describe(args)).to be_a(String)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'selenium-webdriver'
|
|
4
|
+
|
|
5
|
+
module Raider
|
|
6
|
+
module Spec
|
|
7
|
+
# One headless Chrome per process, created after the runtime is required.
|
|
8
|
+
module Browser
|
|
9
|
+
FIXTURES_DIR = File.expand_path('../fixtures', __dir__)
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def driver
|
|
14
|
+
@driver ||= Selenium::WebDriver.for(:chrome, options: options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fixture_url(fixture)
|
|
18
|
+
"file://#{File.expand_path(fixture, FIXTURES_DIR)}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def open(fixture)
|
|
22
|
+
driver.navigate.to(fixture_url(fixture))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def bidi_driver
|
|
26
|
+
@bidi_driver ||= Selenium::WebDriver.for(:chrome, options: options.tap { |chrome| chrome.web_socket_url = true })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def quit_bidi
|
|
30
|
+
@bidi_driver&.quit
|
|
31
|
+
@bidi_driver = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quit
|
|
35
|
+
@driver&.quit
|
|
36
|
+
@driver = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def options
|
|
40
|
+
Selenium::WebDriver::Chrome::Options.new.tap do |chrome|
|
|
41
|
+
%w[--headless=new --no-sandbox --disable-dev-shm-usage --window-size=1280,900].each do |flag|
|
|
42
|
+
chrome.add_argument(flag)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'selenium-webdriver'
|
|
4
|
+
|
|
5
|
+
module Raider
|
|
6
|
+
module Spec
|
|
7
|
+
# A stand-in for Selenium's remote bridge: converts locators the way a real
|
|
8
|
+
# bridge does, records what reaches the wire and answers from a script.
|
|
9
|
+
# :reek:TooManyInstanceVariables { max_instance_variables: 6 } -- a test double: one slot per wire recorder and answer channel
|
|
10
|
+
class FakeBridge
|
|
11
|
+
attr_reader :calls, :script_results, :script_calls
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@locator_converter = Selenium::WebDriver::Remote::Bridge.locator_converter
|
|
15
|
+
@calls = []
|
|
16
|
+
@queue = []
|
|
17
|
+
@script_results = []
|
|
18
|
+
@script_calls = []
|
|
19
|
+
@answers = Hash.new { |hash, key| hash[key] = [] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Scripts per-poll answers for an element command; the last answer repeats.
|
|
23
|
+
# An Exception class or instance is raised instead of returned.
|
|
24
|
+
def answer(command, *values)
|
|
25
|
+
@answers[command].concat(values)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
%i[element_text element_attribute element_displayed? element_enabled? element_selected?].each do |command|
|
|
30
|
+
define_method(command) { |*_args| next_answer(command) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def enqueue(ids)
|
|
34
|
+
@queue << ids
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_element_by(how, what, ref = [])
|
|
38
|
+
how, what = @locator_converter.convert(how, what)
|
|
39
|
+
calls << [:find_element_by, how, what, ref]
|
|
40
|
+
ids = @queue.shift || []
|
|
41
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, 'no such element' if ids.empty?
|
|
42
|
+
|
|
43
|
+
wrap(ids.first)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_elements_by(how, what, ref = [])
|
|
47
|
+
how, what = @locator_converter.convert(how, what)
|
|
48
|
+
calls << [:find_elements_by, how, what, ref]
|
|
49
|
+
(@queue.shift || []).map { |id| wrap(id) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute_script(script, *args)
|
|
53
|
+
script_calls << [script, *args]
|
|
54
|
+
result = script_results.shift
|
|
55
|
+
raise result if result.is_a?(Exception)
|
|
56
|
+
|
|
57
|
+
result
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def click_element(id)
|
|
61
|
+
calls << [:click_element, id]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def send_keys_to_element(id, keys)
|
|
65
|
+
calls << [:send_keys_to_element, id, keys]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def clear_element(id)
|
|
69
|
+
calls << [:clear_element, id]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def submit_element(id)
|
|
73
|
+
calls << [:submit_element, id]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Records the call; answers from the scripted channel, else raises so
|
|
77
|
+
# Describe.element falls back to <element>.
|
|
78
|
+
def element_tag_name(ref)
|
|
79
|
+
calls << [:element_tag_name, ref]
|
|
80
|
+
raise Selenium::WebDriver::Error::WebDriverError, 'no tag' if @answers[:element_tag_name].empty?
|
|
81
|
+
|
|
82
|
+
next_answer(:element_tag_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def element_class
|
|
86
|
+
Selenium::WebDriver::Element
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def next_answer(command)
|
|
92
|
+
queue = @answers[command]
|
|
93
|
+
value = queue.size > 1 ? queue.shift : queue.first
|
|
94
|
+
raise value, 'scripted' if value.is_a?(Class) && value <= Exception
|
|
95
|
+
|
|
96
|
+
value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def wrap(id)
|
|
100
|
+
Selenium::WebDriver::Element.new(self, id)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# An initialized real bridge of klass, built without a network call: the
|
|
105
|
+
# initializer signature differs across selenium versions.
|
|
106
|
+
def self.real_bridge(klass)
|
|
107
|
+
keywords = Selenium::WebDriver::Remote::Bridge.instance_method(:initialize).parameters.map(&:last)
|
|
108
|
+
args = { http_client: Selenium::WebDriver::Remote::Http::Default.new }
|
|
109
|
+
args[:url] = 'http://127.0.0.1:4444/' if keywords.include?(:url)
|
|
110
|
+
klass.allocate.tap { |bridge| bridge.send(:initialize, **args) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.driver(bridge)
|
|
114
|
+
Selenium::WebDriver::Driver.allocate.tap { |driver| driver.instance_variable_set(:@bridge, bridge) }
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|