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
|
@@ -4,12 +4,20 @@ require 'yaml'
|
|
|
4
4
|
<%- if selenium_based? -%>
|
|
5
5
|
require 'active_support/inflector'
|
|
6
6
|
require 'selenium-webdriver'
|
|
7
|
+
require 'ruby_raider/runtime'
|
|
7
8
|
require_relative 'video_helper'
|
|
9
|
+
<%- if network_helper? -%>
|
|
10
|
+
require_relative 'network_helper'
|
|
11
|
+
<%- end -%>
|
|
12
|
+
<%- if screenshot_helper? -%>
|
|
13
|
+
require_relative 'screenshot_helper'
|
|
14
|
+
<%- end -%>
|
|
8
15
|
<%- if trace_helper? -%>
|
|
9
16
|
require_relative 'trace_helper'
|
|
10
17
|
<%- end -%>
|
|
11
18
|
<%- else -%>
|
|
12
19
|
require 'appium_lib'
|
|
20
|
+
require 'ruby_raider/runtime'
|
|
13
21
|
<%- end -%>
|
|
14
22
|
|
|
15
23
|
module DriverHelper
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_raider/runtime'
|
|
4
|
+
|
|
5
|
+
# Swipes and scrolls through the platform's own mobile: commands. Finding and clicking
|
|
6
|
+
# elements is plain Appium; the runtime waits for them.
|
|
7
|
+
#
|
|
8
|
+
# swipe('left', element)
|
|
9
|
+
# scroll('down')
|
|
10
|
+
# scroll_to({ accessibility_id: 'footer' }).click
|
|
11
|
+
module GestureHelper
|
|
12
|
+
# Swipes the element, or the whole screen without one.
|
|
13
|
+
def swipe(direction, element = nil)
|
|
14
|
+
Gesture.new(driver, :swipe, direction).perform(element)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Scrolls the element, or the whole screen without one.
|
|
18
|
+
def scroll(direction, element = nil)
|
|
19
|
+
Gesture.new(driver, :scroll, direction).perform(element)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Scrolls the screen between single attempts to find the element, up to max_scrolls times.
|
|
23
|
+
def scroll_to(locator, direction: 'down', max_scrolls: 10)
|
|
24
|
+
gesture = Gesture.new(driver, :scroll, direction)
|
|
25
|
+
scrolls = 0
|
|
26
|
+
begin
|
|
27
|
+
Raider::Wait.immediately { driver.find_element(locator) }
|
|
28
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
|
29
|
+
if scrolls >= max_scrolls
|
|
30
|
+
raise Selenium::WebDriver::Error::NoSuchElementError,
|
|
31
|
+
"unable to locate #{Raider::Locator.describe([locator])} after #{scrolls} scrolls"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
gesture.perform(nil)
|
|
35
|
+
scrolls += 1
|
|
36
|
+
retry
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A swipe or scroll through the platform's own mobile: command, as appium_lib issues them.
|
|
41
|
+
class Gesture
|
|
42
|
+
DIRECTIONS = %w[up down left right].freeze
|
|
43
|
+
COMMANDS = {
|
|
44
|
+
ios: { swipe: 'mobile: swipe', scroll: 'mobile: scroll' }.freeze,
|
|
45
|
+
android: { swipe: 'mobile: swipeGesture', scroll: 'mobile: scrollGesture' }.freeze
|
|
46
|
+
}.freeze
|
|
47
|
+
PERCENT = 0.75
|
|
48
|
+
|
|
49
|
+
def initialize(driver, kind, direction)
|
|
50
|
+
@driver = driver
|
|
51
|
+
@device = driver.appium_device.to_s.downcase.to_sym
|
|
52
|
+
@command = COMMANDS.dig(@device, kind)
|
|
53
|
+
raise NotImplementedError, "#{kind} has no gesture for #{driver.appium_device.inspect}; only :ios and :android" unless @command
|
|
54
|
+
|
|
55
|
+
@direction = direction.to_s
|
|
56
|
+
raise ArgumentError, "direction must be up, down, left or right; got #{direction.inspect}" unless DIRECTIONS.include?(@direction)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# element: the raw element to act on, or nil for the whole screen.
|
|
60
|
+
def perform(element)
|
|
61
|
+
@driver.execute_script(@command, arguments(element))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def arguments(element)
|
|
67
|
+
return { direction: @direction, element: element&.id }.compact if @device == :ios
|
|
68
|
+
|
|
69
|
+
area = element ? { elementId: element.id } : screen
|
|
70
|
+
area.merge(direction: @direction, percent: PERCENT)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def screen
|
|
74
|
+
rect = @driver.window_rect
|
|
75
|
+
{ left: rect.x, top: rect.y, width: rect.width, height: rect.height }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
# Mocks network requests in the browser through WebDriver BiDi (chrome, edge and firefox):
|
|
7
|
+
# answer a request with your own response, or block it.
|
|
8
|
+
# BIDI=off turns BiDi off, for a grid that cannot carry the websocket.
|
|
9
|
+
module NetworkHelper
|
|
10
|
+
# Raised when mocking cannot run: no BiDi, or a browser without it.
|
|
11
|
+
class Unavailable < StandardError; end
|
|
12
|
+
|
|
13
|
+
# Raised for a mock that cannot be set up: a bad pattern or an invalid response.
|
|
14
|
+
class MockError < StandardError; end
|
|
15
|
+
|
|
16
|
+
# Matches a request against a glob or Regexp URL and an optional HTTP method.
|
|
17
|
+
# Glob: ** is any characters, * is any characters except /, everything else is literal.
|
|
18
|
+
class Matcher
|
|
19
|
+
def initialize(url, method: nil)
|
|
20
|
+
raise ArgumentError, "url must be a String or Regexp, got #{url.class}" unless url.is_a?(String) || url.is_a?(Regexp)
|
|
21
|
+
|
|
22
|
+
@url = url
|
|
23
|
+
@method = method&.to_s&.upcase
|
|
24
|
+
@regexp = url.is_a?(Regexp) ? url : glob_to_regexp(url)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match?(url_string, http_method)
|
|
28
|
+
return false if @method && @method != http_method.to_s.upcase
|
|
29
|
+
|
|
30
|
+
match_url?(url_string)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def match_url?(url_string)
|
|
34
|
+
@regexp.match?(url_string.to_s)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def method?(http_method)
|
|
38
|
+
@method == http_method.to_s.upcase
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# BiDi urlPatterns narrowing the intercept to the literal parts of the glob, or nil to pause everything.
|
|
42
|
+
def intercept_patterns
|
|
43
|
+
return nil if @url.is_a?(Regexp)
|
|
44
|
+
|
|
45
|
+
pattern = literal_parts.compact
|
|
46
|
+
pattern.empty? ? nil : [{ type: 'pattern' }.merge(pattern)]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Characters a BiDi URL pattern pathname accepts as literals; anything else is left to the matcher.
|
|
50
|
+
SAFE_PATH = %r{\A[A-Za-z0-9._~!$&'+,;=:@/%-]*\z}.freeze
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def glob_to_regexp(glob)
|
|
55
|
+
source = glob.split(/(\*\*|\*)/).map do |token|
|
|
56
|
+
case token
|
|
57
|
+
when '**' then '.*'
|
|
58
|
+
when '*' then '[^/]*'
|
|
59
|
+
else Regexp.escape(token)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
Regexp.new("\\A#{source.join}\\z", Regexp::MULTILINE)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Only a glob that opens with a literal scheme://host[:port] narrows: anything else could match a
|
|
66
|
+
# URL the pattern would miss. The path counts only right after such an origin.
|
|
67
|
+
def literal_parts
|
|
68
|
+
glob = @url.split(/[?#]/, 2).first.to_s
|
|
69
|
+
scheme = glob[%r{\A([a-z][a-z0-9+.-]*)://}i, 1]
|
|
70
|
+
return {} unless scheme
|
|
71
|
+
|
|
72
|
+
hostport, path = glob.delete_prefix("#{scheme}://").split(%r{(?=/)}, 2)
|
|
73
|
+
return {} if hostport.to_s.match?(/[\[@]/)
|
|
74
|
+
|
|
75
|
+
host, port = hostport.to_s.split(':', 2)
|
|
76
|
+
return {} if [host, port].compact.any? { |part| part.empty? || part.include?('*') }
|
|
77
|
+
|
|
78
|
+
{ protocol: scheme.downcase, hostname: host, port: port, pathname: literal_path(path) }.compact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def literal_path(path)
|
|
82
|
+
path if path&.match?(SAFE_PATH)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The driver helper sets web_socket_url for these.
|
|
87
|
+
BIDI_BROWSERS = %w[chrome edge firefox].freeze
|
|
88
|
+
|
|
89
|
+
REASON_PHRASES = {
|
|
90
|
+
200 => 'OK', 201 => 'Created', 204 => 'No Content', 301 => 'Moved Permanently', 302 => 'Found',
|
|
91
|
+
304 => 'Not Modified', 400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden',
|
|
92
|
+
404 => 'Not Found', 409 => 'Conflict', 422 => 'Unprocessable Entity', 429 => 'Too Many Requests',
|
|
93
|
+
500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable',
|
|
94
|
+
504 => 'Gateway Timeout'
|
|
95
|
+
}.freeze
|
|
96
|
+
|
|
97
|
+
# A request the browser paused: lowercase header names, values as strings. Frozen.
|
|
98
|
+
RaiderRequest = Struct.new(:method, :url, :headers) # rubocop:disable Lint/StructNewOverride
|
|
99
|
+
|
|
100
|
+
# What a mock answered or will answer. calls holds the requests it handled, oldest first.
|
|
101
|
+
class RaiderMock
|
|
102
|
+
attr_reader :url, :matcher, :response, :block
|
|
103
|
+
|
|
104
|
+
def initialize(url, matcher, response: nil, block: nil)
|
|
105
|
+
@url = url
|
|
106
|
+
@matcher = matcher
|
|
107
|
+
@response = response
|
|
108
|
+
@block = block
|
|
109
|
+
@calls = []
|
|
110
|
+
@lock = Mutex.new
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def calls
|
|
114
|
+
@lock.synchronize { @calls.dup.freeze }
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def called?
|
|
118
|
+
@lock.synchronize { !@calls.empty? }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def record(request)
|
|
122
|
+
@lock.synchronize { @calls << request }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# One per driver. Holds the mocks and answers paused requests on Selenium's callback threads.
|
|
127
|
+
# All state sits behind one Mutex; a mock_request block runs outside it.
|
|
128
|
+
class Router
|
|
129
|
+
EVENT = 'network.beforeRequestSent'
|
|
130
|
+
FATAL = [Interrupt, SystemExit, NoMemoryError].freeze
|
|
131
|
+
|
|
132
|
+
def initialize(bidi)
|
|
133
|
+
@bidi = bidi
|
|
134
|
+
@lock = Mutex.new
|
|
135
|
+
@mocks = []
|
|
136
|
+
@intercepts = {}
|
|
137
|
+
@ids = []
|
|
138
|
+
@error = nil
|
|
139
|
+
bidi.session.subscribe(EVENT)
|
|
140
|
+
bidi.add_callback(EVENT) { |params| handle(params) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def add(mock)
|
|
144
|
+
@lock.synchronize do
|
|
145
|
+
params = { phases: ['beforeRequestSent'] }
|
|
146
|
+
patterns = mock.matcher.intercept_patterns
|
|
147
|
+
params[:urlPatterns] = patterns if patterns
|
|
148
|
+
id = @bidi.send_cmd('network.addIntercept', **params)['intercept']
|
|
149
|
+
@mocks << mock
|
|
150
|
+
@intercepts[mock] = id
|
|
151
|
+
@ids << id
|
|
152
|
+
end
|
|
153
|
+
mock
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def remove(mock)
|
|
157
|
+
id = @lock.synchronize do
|
|
158
|
+
@mocks.delete(mock)
|
|
159
|
+
@intercepts.delete(mock)
|
|
160
|
+
end
|
|
161
|
+
remove_intercept(id) if id
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def clear
|
|
166
|
+
ids = @lock.synchronize do
|
|
167
|
+
@mocks.clear
|
|
168
|
+
@intercepts.values.tap { @intercepts.clear }
|
|
169
|
+
end
|
|
170
|
+
ids.each { |id| remove_intercept(id) }
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def raise_error!
|
|
175
|
+
error = @lock.synchronize { @error }
|
|
176
|
+
raise error if error
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Removes every intercept and hands back the stored error, if any. Never raises.
|
|
180
|
+
def finish
|
|
181
|
+
ids, error = @lock.synchronize do
|
|
182
|
+
taken = [@intercepts.values, @error]
|
|
183
|
+
@mocks.clear
|
|
184
|
+
@intercepts.clear
|
|
185
|
+
@error = nil
|
|
186
|
+
taken
|
|
187
|
+
end
|
|
188
|
+
ids.each { |id| remove_intercept(id) }
|
|
189
|
+
error
|
|
190
|
+
rescue StandardError
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def remove_intercept(id)
|
|
197
|
+
@bidi.send_cmd('network.removeIntercept', intercept: id)
|
|
198
|
+
rescue StandardError
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def handle(params)
|
|
203
|
+
return unless params['isBlocked'] && ours?(params['intercepts'])
|
|
204
|
+
|
|
205
|
+
answer = [:fail]
|
|
206
|
+
begin
|
|
207
|
+
answer = decide(params['request'] || {})
|
|
208
|
+
rescue StandardError => e
|
|
209
|
+
store_error(e)
|
|
210
|
+
ensure
|
|
211
|
+
deliver(params, answer)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def ours?(ids)
|
|
216
|
+
@lock.synchronize { Array(ids).any? { |id| @ids.include?(id) } }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def store_error(error)
|
|
220
|
+
@lock.synchronize { @error = error if @error.nil? }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def decide(raw)
|
|
224
|
+
request = build_request(raw)
|
|
225
|
+
mocks, error = @lock.synchronize { [@mocks.reverse, @error] }
|
|
226
|
+
mock = mocks.find { |m| m.matcher.match?(request.url, request.method) }
|
|
227
|
+
preflight = preflight?(request, mocks)
|
|
228
|
+
return (mock || preflight ? [:fail] : [:continue, nil]) if error
|
|
229
|
+
return preflight_answer(request) if preflight
|
|
230
|
+
return [:continue, nil] unless mock
|
|
231
|
+
|
|
232
|
+
mock.record(request)
|
|
233
|
+
answer = mock.block ? from_block(mock, request, raw['headers']) : [:fulfil, mock.response]
|
|
234
|
+
answer.first == :fulfil ? [:fulfil, with_cors(request, answer.last)] : answer
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# An OPTIONS request asking for CORS permission to a URL some mock covers, with no mock of
|
|
238
|
+
# its own for OPTIONS: the router answers it. It is neither recorded nor reported.
|
|
239
|
+
def preflight?(request, mocks)
|
|
240
|
+
return false unless request.method.upcase == 'OPTIONS' && request.headers.key?('access-control-request-method')
|
|
241
|
+
|
|
242
|
+
covering = mocks.select { |m| m.matcher.match_url?(request.url) }
|
|
243
|
+
covering.any? && covering.none? { |m| m.matcher.method?('OPTIONS') }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def preflight_answer(request)
|
|
247
|
+
pairs = [['Access-Control-Allow-Methods', request.headers['access-control-request-method']]]
|
|
248
|
+
requested = request.headers['access-control-request-headers']
|
|
249
|
+
pairs << ['Access-Control-Allow-Headers', requested] if requested
|
|
250
|
+
[:preflight, with_cors(request, { status: 204, headers: pairs, body: '' }, force: true)]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Adds Allow-Origin and Allow-Credentials for a cross-origin request unless the mock set them.
|
|
254
|
+
def with_cors(request, response, force: false)
|
|
255
|
+
origin = request_origin(request)
|
|
256
|
+
return response if origin && !(force || cross_origin?(origin, request.url))
|
|
257
|
+
|
|
258
|
+
headers = response[:headers].dup
|
|
259
|
+
# Without a known origin (Referrer-Policy no-referrer) a wildcard passes, but never with credentials.
|
|
260
|
+
allowed = { 'Access-Control-Allow-Origin' => origin || '*' }
|
|
261
|
+
allowed['Access-Control-Allow-Credentials'] = 'true' if origin
|
|
262
|
+
allowed.each do |name, value|
|
|
263
|
+
headers << [name, value] if headers.none? { |existing, _| existing.casecmp?(name) }
|
|
264
|
+
end
|
|
265
|
+
response.merge(headers: headers)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Chrome adds the Origin header after the request is paused, so it is missing here: the Referer's
|
|
269
|
+
# origin stands in for it.
|
|
270
|
+
def request_origin(request)
|
|
271
|
+
request.headers['origin'] || referer_origin(request.headers['referer'])
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def referer_origin(referer)
|
|
275
|
+
return nil unless referer
|
|
276
|
+
|
|
277
|
+
uri = URI(referer)
|
|
278
|
+
"#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}" if uri.host
|
|
279
|
+
rescue URI::Error
|
|
280
|
+
nil
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def cross_origin?(origin, url)
|
|
284
|
+
uri = URI(url)
|
|
285
|
+
own = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
|
|
286
|
+
origin.downcase != own.downcase
|
|
287
|
+
rescue URI::Error
|
|
288
|
+
false
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_request(raw)
|
|
292
|
+
headers = Array(raw['headers']).each_with_object({}) do |header, all|
|
|
293
|
+
all[header['name'].to_s.downcase] = header_value(header)
|
|
294
|
+
end
|
|
295
|
+
RaiderRequest.new(raw['method'].to_s, raw['url'].to_s, headers.freeze).freeze
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def header_value(header)
|
|
299
|
+
value = header['value']
|
|
300
|
+
value.is_a?(Hash) ? value['value'] : value
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def from_block(mock, request, raw_headers)
|
|
304
|
+
result = run_block(mock, request)
|
|
305
|
+
case result
|
|
306
|
+
when :fail then [:fail]
|
|
307
|
+
when :continue then [:continue, nil]
|
|
308
|
+
when Hash then from_hash(mock, result, raw_headers)
|
|
309
|
+
else junk(mock, result)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def run_block(mock, request)
|
|
314
|
+
mock.block.call(request)
|
|
315
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
316
|
+
# An expectation failure inside the block is an Exception, not a StandardError: it must not
|
|
317
|
+
# vanish on Selenium's callback thread. Fatal ones fail the request (ensure) and propagate.
|
|
318
|
+
raise if FATAL.any? { |fatal| e.is_a?(fatal) }
|
|
319
|
+
|
|
320
|
+
raise MockError, "mock_request block for #{mock.url} raised #{e.class}: #{e.message}"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def from_hash(mock, result, raw_headers)
|
|
324
|
+
return continue_with(mock, result, raw_headers) if result.key?(:continue)
|
|
325
|
+
|
|
326
|
+
[:fulfil, NetworkHelper.build_response(**result)]
|
|
327
|
+
rescue ArgumentError, TypeError, NoMethodError => e
|
|
328
|
+
junk(mock, result, e.message)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def continue_with(mock, result, raw_headers)
|
|
332
|
+
options = result[:continue]
|
|
333
|
+
return junk(mock, result) unless result.size == 1 && options.is_a?(Hash) && (options.keys - [:headers]).empty?
|
|
334
|
+
|
|
335
|
+
[:continue, NetworkHelper.merge_headers(raw_headers, options[:headers] || {})]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def junk(mock, result, reason = nil)
|
|
339
|
+
raise MockError, ["mock_request block for #{mock.url} returned #{result.inspect}", reason].compact.join(': ')
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def deliver(params, answer)
|
|
343
|
+
request_id = params['request']['request']
|
|
344
|
+
kind, detail = answer
|
|
345
|
+
case kind
|
|
346
|
+
when :fulfil then respond(params, request_id, detail)
|
|
347
|
+
when :preflight then provide(request_id, detail)
|
|
348
|
+
when :fail then fail_request(params, request_id)
|
|
349
|
+
else continue_request(request_id, detail)
|
|
350
|
+
end
|
|
351
|
+
rescue StandardError
|
|
352
|
+
nil
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def respond(params, request_id, response)
|
|
356
|
+
notify(params, request_id, 'fulfilled', response[:status])
|
|
357
|
+
provide(request_id, response)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def provide(request_id, response)
|
|
361
|
+
@bidi.send_cmd('network.provideResponse', request: request_id, statusCode: response[:status],
|
|
362
|
+
reasonPhrase: REASON_PHRASES.fetch(response[:status], 'OK'),
|
|
363
|
+
headers: NetworkHelper.header_list(response[:headers]),
|
|
364
|
+
body: { type: 'base64', value: [response[:body]].pack('m0') })
|
|
365
|
+
rescue StandardError
|
|
366
|
+
# The page must never hang on a paused request: fall back to a network error.
|
|
367
|
+
begin
|
|
368
|
+
@bidi.send_cmd('network.failRequest', request: request_id)
|
|
369
|
+
rescue StandardError
|
|
370
|
+
nil
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def fail_request(params, request_id)
|
|
375
|
+
notify(params, request_id, 'failed', nil)
|
|
376
|
+
@bidi.send_cmd('network.failRequest', request: request_id)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def continue_request(request_id, headers)
|
|
380
|
+
args = { request: request_id }
|
|
381
|
+
args[:headers] = headers if headers
|
|
382
|
+
@bidi.send_cmd('network.continueRequest', **args)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def notify(params, request_id, outcome, status)
|
|
386
|
+
NetworkHelper.observer&.request_mocked(request_id: request_id, redirect_count: params['redirectCount'].to_i,
|
|
387
|
+
outcome: outcome, status: status)
|
|
388
|
+
rescue StandardError
|
|
389
|
+
nil
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
ROUTERS = ObjectSpace::WeakMap.new
|
|
394
|
+
ROUTERS_LOCK = Mutex.new
|
|
395
|
+
|
|
396
|
+
class << self
|
|
397
|
+
# Follows the mocks: responds to #request_mocked(request_id:, redirect_count:, outcome:, status:)
|
|
398
|
+
# when set. Called before a request is answered. Nil by default.
|
|
399
|
+
attr_accessor :observer
|
|
400
|
+
|
|
401
|
+
def bidi_off?
|
|
402
|
+
ENV['BIDI'].to_s.downcase == 'off'
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# True for a browser that speaks WebDriver BiDi (Edge reports MicrosoftEdge), unless BIDI=off.
|
|
406
|
+
def bidi?(browser)
|
|
407
|
+
return false if bidi_off?
|
|
408
|
+
|
|
409
|
+
name = browser.to_s.downcase.delete_prefix('microsoft')
|
|
410
|
+
BIDI_BROWSERS.any? { |bidi_browser| name.start_with?(bidi_browser) }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# The router for this driver, created on first use. Raises Unavailable without BiDi.
|
|
414
|
+
# ROUTERS holds its values weakly; the router stays alive because the block registered with
|
|
415
|
+
# bidi.add_callback in Router#initialize captures it, and the driver's bidi holds that block.
|
|
416
|
+
def router_for(driver)
|
|
417
|
+
ROUTERS_LOCK.synchronize { ROUTERS[driver] ||= create_router(driver) }
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# Removes the driver's intercepts and returns the stored MockError, or nil. Never raises.
|
|
421
|
+
def finish(driver)
|
|
422
|
+
router = ROUTERS_LOCK.synchronize { ROUTERS[driver] }
|
|
423
|
+
router&.finish
|
|
424
|
+
rescue StandardError
|
|
425
|
+
nil
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Adds the mock to the driver's router, raising a stored MockError first.
|
|
429
|
+
def register(driver, mock)
|
|
430
|
+
router = router_for(driver)
|
|
431
|
+
router.raise_error!
|
|
432
|
+
router.add(mock)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# The router if one exists; never creates one.
|
|
436
|
+
def existing_router(driver)
|
|
437
|
+
ROUTERS_LOCK.synchronize { ROUTERS[driver] }
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def build_response(status: 200, json: nil, body: nil, headers: {}, content_type: nil)
|
|
441
|
+
raise ArgumentError, 'pass json: or body:, not both' unless json.nil? || body.nil?
|
|
442
|
+
|
|
443
|
+
list = headers.map { |name, value| [name.to_s, value.to_s] }
|
|
444
|
+
list = list.reject { |name, _| name.casecmp?('content-type') } << ['Content-Type', content_type.to_s] if content_type
|
|
445
|
+
list << ['Content-Type', 'application/json'] if !json.nil? && list.none? { |name, _| name.casecmp?('content-type') }
|
|
446
|
+
{ status: valid_status(status), headers: list, body: (json.nil? ? body.to_s : JSON.generate(json)) }
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def valid_status(status)
|
|
450
|
+
code = Integer(status)
|
|
451
|
+
raise ArgumentError, "status must be 100..599, got #{code}" unless (100..599).cover?(code)
|
|
452
|
+
|
|
453
|
+
code
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# The request's own headers with these replacing same-named ones, as a BiDi header list.
|
|
457
|
+
def merge_headers(raw_headers, changes)
|
|
458
|
+
names = changes.keys.map { |name| name.to_s.downcase }
|
|
459
|
+
kept = Array(raw_headers).reject { |header| names.include?(header['name'].to_s.downcase) }
|
|
460
|
+
kept = kept.map do |header|
|
|
461
|
+
value = header['value'].is_a?(Hash) ? header['value'] : { 'type' => 'string', 'value' => header['value'] }
|
|
462
|
+
{ name: header['name'], value: { type: value['type'], value: value['value'] } }
|
|
463
|
+
end
|
|
464
|
+
kept + header_list(changes.map { |name, value| [name.to_s, value.to_s] })
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def header_list(pairs)
|
|
468
|
+
pairs.map { |name, value| { name: name, value: { type: 'string', value: value } } }
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
private
|
|
472
|
+
|
|
473
|
+
def create_router(driver)
|
|
474
|
+
raise Unavailable, 'network mocking is off: BIDI=off is set' if bidi_off?
|
|
475
|
+
|
|
476
|
+
browser = driver.capabilities.browser_name
|
|
477
|
+
raise Unavailable, "network mocking needs WebDriver BiDi, which #{browser.inspect} does not support" unless bidi?(browser)
|
|
478
|
+
|
|
479
|
+
begin
|
|
480
|
+
Router.new(driver.bidi)
|
|
481
|
+
rescue StandardError => e
|
|
482
|
+
raise Unavailable, "network mocking needs WebDriver BiDi, but #{browser} refused it: #{e.class}: #{e.message}"
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Answers matching requests with your own response; the request never reaches the server.
|
|
488
|
+
# Pass a block instead to decide per request: it gets a RaiderRequest and returns a Hash of
|
|
489
|
+
# response options, :fail, :continue, or { continue: { headers: { 'x-a' => 'b' } } }.
|
|
490
|
+
def mock_request(url, status: 200, json: nil, body: nil, headers: {}, content_type: nil, method: nil, &block)
|
|
491
|
+
matcher = Matcher.new(url, method: method)
|
|
492
|
+
response = nil
|
|
493
|
+
given = status != 200 || !json.nil? || !body.nil? || !headers.empty? || content_type
|
|
494
|
+
raise ArgumentError, 'give either a block or a response, not both' if block && given
|
|
495
|
+
|
|
496
|
+
unless block
|
|
497
|
+
response = NetworkHelper.build_response(status: status, json: json, body: body,
|
|
498
|
+
headers: headers, content_type: content_type)
|
|
499
|
+
end
|
|
500
|
+
NetworkHelper.register(driver, RaiderMock.new(url, matcher, response: response, block: block))
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# Matching requests fail with a network error.
|
|
504
|
+
def block_request(url, method: nil)
|
|
505
|
+
mock_request(url, method: method) { :fail }
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Stops answering for this mock and removes its intercept.
|
|
509
|
+
def remove_mock(mock)
|
|
510
|
+
router = NetworkHelper.existing_router(driver)
|
|
511
|
+
router&.raise_error!
|
|
512
|
+
router&.remove(mock)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Removes every mock on this driver.
|
|
516
|
+
def clear_mocks
|
|
517
|
+
router = NetworkHelper.existing_router(driver)
|
|
518
|
+
router&.raise_error!
|
|
519
|
+
router&.clear
|
|
520
|
+
end
|
|
521
|
+
end
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
browser = load_browser == 'ie' ? load_browser.upcase : load_browser.capitalize
|
|
27
27
|
options = "Selenium::WebDriver::#{browser}::Options".constantize.new
|
|
28
28
|
browser_arguments(*opts).each { |arg| options.add_argument(arg) }
|
|
29
|
-
# WebDriver BiDi carries the trace's network and console (chrome, edge and firefox).
|
|
30
|
-
|
|
29
|
+
# WebDriver BiDi carries the trace's network and console and the network mocks (chrome, edge and firefox).
|
|
30
|
+
# BIDI=off turns it off for a grid that cannot carry the websocket.
|
|
31
|
+
bidi = (defined?(TraceHelper) && TraceHelper.bidi?(load_browser)) ||
|
|
32
|
+
(defined?(NetworkHelper) && NetworkHelper.bidi?(load_browser))
|
|
33
|
+
options.web_socket_url = true if bidi
|
|
31
34
|
options
|
|
32
35
|
end
|