bbc-selenium-webdriver 1.17.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.
- data/CHANGES +473 -0
- data/README +31 -0
- data/lib/selenium-client.rb +2 -0
- data/lib/selenium-webdriver.rb +1 -0
- data/lib/selenium/client.rb +37 -0
- data/lib/selenium/client/base.rb +118 -0
- data/lib/selenium/client/driver.rb +10 -0
- data/lib/selenium/client/errors.rb +9 -0
- data/lib/selenium/client/extensions.rb +118 -0
- data/lib/selenium/client/idiomatic.rb +488 -0
- data/lib/selenium/client/javascript_expression_builder.rb +116 -0
- data/lib/selenium/client/javascript_frameworks/jquery.rb +13 -0
- data/lib/selenium/client/javascript_frameworks/prototype.rb +13 -0
- data/lib/selenium/client/legacy_driver.rb +1720 -0
- data/lib/selenium/client/protocol.rb +104 -0
- data/lib/selenium/client/selenium_helper.rb +34 -0
- data/lib/selenium/rake/server_task.rb +157 -0
- data/lib/selenium/server.rb +223 -0
- data/lib/selenium/webdriver.rb +64 -0
- data/lib/selenium/webdriver/android.rb +9 -0
- data/lib/selenium/webdriver/android/bridge.rb +51 -0
- data/lib/selenium/webdriver/chrome.rb +26 -0
- data/lib/selenium/webdriver/chrome/bridge.rb +106 -0
- data/lib/selenium/webdriver/chrome/profile.rb +83 -0
- data/lib/selenium/webdriver/chrome/service.rb +74 -0
- data/lib/selenium/webdriver/common.rb +30 -0
- data/lib/selenium/webdriver/common/action_builder.rb +352 -0
- data/lib/selenium/webdriver/common/alert.rb +26 -0
- data/lib/selenium/webdriver/common/bridge_helper.rb +67 -0
- data/lib/selenium/webdriver/common/core_ext/base64.rb +9 -0
- data/lib/selenium/webdriver/common/core_ext/dir.rb +42 -0
- data/lib/selenium/webdriver/common/core_ext/string.rb +5 -0
- data/lib/selenium/webdriver/common/driver.rb +301 -0
- data/lib/selenium/webdriver/common/driver_extensions/has_input_devices.rb +39 -0
- data/lib/selenium/webdriver/common/driver_extensions/rotatable.rb +44 -0
- data/lib/selenium/webdriver/common/driver_extensions/takes_screenshot.rb +43 -0
- data/lib/selenium/webdriver/common/driver_extensions/uploads_files.rb +46 -0
- data/lib/selenium/webdriver/common/element.rb +262 -0
- data/lib/selenium/webdriver/common/error.rb +212 -0
- data/lib/selenium/webdriver/common/file_reaper.rb +47 -0
- data/lib/selenium/webdriver/common/keyboard.rb +52 -0
- data/lib/selenium/webdriver/common/keys.rb +109 -0
- data/lib/selenium/webdriver/common/mouse.rb +65 -0
- data/lib/selenium/webdriver/common/navigation.rb +43 -0
- data/lib/selenium/webdriver/common/options.rb +128 -0
- data/lib/selenium/webdriver/common/platform.rb +166 -0
- data/lib/selenium/webdriver/common/port_prober.rb +25 -0
- data/lib/selenium/webdriver/common/profile_helper.rb +67 -0
- data/lib/selenium/webdriver/common/proxy.rb +120 -0
- data/lib/selenium/webdriver/common/search_context.rb +92 -0
- data/lib/selenium/webdriver/common/socket_poller.rb +100 -0
- data/lib/selenium/webdriver/common/target_locator.rb +81 -0
- data/lib/selenium/webdriver/common/timeouts.rb +29 -0
- data/lib/selenium/webdriver/common/wait.rb +60 -0
- data/lib/selenium/webdriver/common/window.rb +96 -0
- data/lib/selenium/webdriver/common/zipper.rb +82 -0
- data/lib/selenium/webdriver/firefox.rb +37 -0
- data/lib/selenium/webdriver/firefox/binary.rb +161 -0
- data/lib/selenium/webdriver/firefox/bridge.rb +58 -0
- data/lib/selenium/webdriver/firefox/extension.rb +52 -0
- data/lib/selenium/webdriver/firefox/extension/webdriver.xpi +0 -0
- data/lib/selenium/webdriver/firefox/launcher.rb +104 -0
- data/lib/selenium/webdriver/firefox/native/linux/amd64/x_ignore_nofocus.so +0 -0
- data/lib/selenium/webdriver/firefox/native/linux/x86/x_ignore_nofocus.so +0 -0
- data/lib/selenium/webdriver/firefox/profile.rb +315 -0
- data/lib/selenium/webdriver/firefox/profiles_ini.rb +62 -0
- data/lib/selenium/webdriver/firefox/socket_lock.rb +61 -0
- data/lib/selenium/webdriver/firefox/util.rb +29 -0
- data/lib/selenium/webdriver/ie.rb +19 -0
- data/lib/selenium/webdriver/ie/bridge.rb +60 -0
- data/lib/selenium/webdriver/ie/native/win32/IEDriver.dll +0 -0
- data/lib/selenium/webdriver/ie/native/x64/IEDriver.dll +0 -0
- data/lib/selenium/webdriver/ie/server.rb +64 -0
- data/lib/selenium/webdriver/iphone.rb +9 -0
- data/lib/selenium/webdriver/iphone/bridge.rb +50 -0
- data/lib/selenium/webdriver/opera.rb +24 -0
- data/lib/selenium/webdriver/opera/bridge.rb +113 -0
- data/lib/selenium/webdriver/opera/service.rb +49 -0
- data/lib/selenium/webdriver/remote.rb +21 -0
- data/lib/selenium/webdriver/remote/bridge.rb +473 -0
- data/lib/selenium/webdriver/remote/capabilities.rb +241 -0
- data/lib/selenium/webdriver/remote/commands.rb +181 -0
- data/lib/selenium/webdriver/remote/http/common.rb +74 -0
- data/lib/selenium/webdriver/remote/http/curb.rb +79 -0
- data/lib/selenium/webdriver/remote/http/default.rb +102 -0
- data/lib/selenium/webdriver/remote/http/persistent.rb +38 -0
- data/lib/selenium/webdriver/remote/response.rb +99 -0
- data/lib/selenium/webdriver/remote/server_error.rb +17 -0
- data/lib/selenium/webdriver/support.rb +4 -0
- data/lib/selenium/webdriver/support/abstract_event_listener.rb +28 -0
- data/lib/selenium/webdriver/support/block_event_listener.rb +17 -0
- data/lib/selenium/webdriver/support/event_firing_bridge.rb +112 -0
- data/lib/selenium/webdriver/support/select.rb +293 -0
- data/spec/integration/selenium/client/api/backward_compatible_api_spec.rb +22 -0
- data/spec/integration/selenium/client/api/browser_xpath_library_spec.rb +17 -0
- data/spec/integration/selenium/client/api/click_spec.rb +39 -0
- data/spec/integration/selenium/client/api/cookie_spec.rb +39 -0
- data/spec/integration/selenium/client/api/element_spec.rb +14 -0
- data/spec/integration/selenium/client/api/highlight_located_element_spec.rb +19 -0
- data/spec/integration/selenium/client/api/retrieve_last_remote_control_logs_spec.rb +33 -0
- data/spec/integration/selenium/client/api/screenshot_spec.rb +29 -0
- data/spec/integration/selenium/client/api/select_window_spec.rb +46 -0
- data/spec/integration/selenium/client/api/start_stop_spec.rb +9 -0
- data/spec/integration/selenium/client/api/wait_for_ajax_spec.rb +27 -0
- data/spec/integration/selenium/client/api/wait_for_element_spec.rb +56 -0
- data/spec/integration/selenium/client/api/wait_for_field_value_spec.rb +52 -0
- data/spec/integration/selenium/client/api/wait_for_text_spec.rb +98 -0
- data/spec/integration/selenium/client/sample-app/public/jquery-1.3.2.js +4376 -0
- data/spec/integration/selenium/client/sample-app/public/jquery.html +55 -0
- data/spec/integration/selenium/client/sample-app/public/prototype-1.6.0.3.js +4320 -0
- data/spec/integration/selenium/client/sample-app/public/prototype.html +59 -0
- data/spec/integration/selenium/client/sample-app/sample_app.rb +32 -0
- data/spec/integration/selenium/client/spec_helper.rb +125 -0
- data/spec/integration/selenium/webdriver/chrome/driver_spec.rb +28 -0
- data/spec/integration/selenium/webdriver/chrome/profile_spec.rb +42 -0
- data/spec/integration/selenium/webdriver/driver_spec.rb +253 -0
- data/spec/integration/selenium/webdriver/element_spec.rb +185 -0
- data/spec/integration/selenium/webdriver/error_spec.rb +30 -0
- data/spec/integration/selenium/webdriver/firefox/driver_spec.rb +21 -0
- data/spec/integration/selenium/webdriver/firefox/profile_spec.rb +141 -0
- data/spec/integration/selenium/webdriver/keyboard_spec.rb +57 -0
- data/spec/integration/selenium/webdriver/mouse_spec.rb +55 -0
- data/spec/integration/selenium/webdriver/navigation_spec.rb +44 -0
- data/spec/integration/selenium/webdriver/opera/driver_spec.rb +47 -0
- data/spec/integration/selenium/webdriver/options_spec.rb +49 -0
- data/spec/integration/selenium/webdriver/remote/element_spec.rb +24 -0
- data/spec/integration/selenium/webdriver/spec_helper.rb +44 -0
- data/spec/integration/selenium/webdriver/spec_support.rb +13 -0
- data/spec/integration/selenium/webdriver/spec_support/guards.rb +86 -0
- data/spec/integration/selenium/webdriver/spec_support/helpers.rb +46 -0
- data/spec/integration/selenium/webdriver/spec_support/jruby_test_environment.rb +29 -0
- data/spec/integration/selenium/webdriver/spec_support/rack_server.rb +123 -0
- data/spec/integration/selenium/webdriver/spec_support/test_environment.rb +199 -0
- data/spec/integration/selenium/webdriver/target_locator_spec.rb +170 -0
- data/spec/integration/selenium/webdriver/timeout_spec.rb +56 -0
- data/spec/integration/selenium/webdriver/window_spec.rb +56 -0
- data/spec/integration/selenium/webdriver/zipper_spec.rb +66 -0
- data/spec/unit/selenium/client/base_spec.rb +239 -0
- data/spec/unit/selenium/client/extensions_spec.rb +174 -0
- data/spec/unit/selenium/client/idiomatic_spec.rb +500 -0
- data/spec/unit/selenium/client/javascript_expression_builder_spec.rb +79 -0
- data/spec/unit/selenium/client/javascript_frameworks/jquery_spec.rb +10 -0
- data/spec/unit/selenium/client/javascript_frameworks/prototype_spec.rb +10 -0
- data/spec/unit/selenium/client/protocol_spec.rb +124 -0
- data/spec/unit/selenium/client/selenium_helper_spec.rb +56 -0
- data/spec/unit/selenium/client/spec_helper.rb +24 -0
- data/spec/unit/selenium/rake/task_spec.rb +79 -0
- data/spec/unit/selenium/server_spec.rb +131 -0
- data/spec/unit/selenium/webdriver/action_builder_spec.rb +90 -0
- data/spec/unit/selenium/webdriver/android/bridge_spec.rb +31 -0
- data/spec/unit/selenium/webdriver/chrome/bridge_spec.rb +94 -0
- data/spec/unit/selenium/webdriver/chrome/profile_spec.rb +57 -0
- data/spec/unit/selenium/webdriver/chrome/service_spec.rb +44 -0
- data/spec/unit/selenium/webdriver/error_spec.rb +41 -0
- data/spec/unit/selenium/webdriver/firefox/bridge_spec.rb +17 -0
- data/spec/unit/selenium/webdriver/iphone/bridge_spec.rb +30 -0
- data/spec/unit/selenium/webdriver/proxy_spec.rb +87 -0
- data/spec/unit/selenium/webdriver/remote/bridge_spec.rb +16 -0
- data/spec/unit/selenium/webdriver/remote/capabilities_spec.rb +111 -0
- data/spec/unit/selenium/webdriver/remote/http/common_spec.rb +24 -0
- data/spec/unit/selenium/webdriver/remote/http/default_spec.rb +46 -0
- data/spec/unit/selenium/webdriver/search_context_spec.rb +61 -0
- data/spec/unit/selenium/webdriver/socket_poller_spec.rb +68 -0
- data/spec/unit/selenium/webdriver/spec_helper.rb +6 -0
- data/spec/unit/selenium/webdriver/support/event_firing_spec.rb +111 -0
- data/spec/unit/selenium/webdriver/support/select_spec.rb +290 -0
- data/spec/unit/selenium/webdriver/wait_spec.rb +40 -0
- metadata +215 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require 'timeout'
|
|
2
|
+
require 'socket'
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
require 'selenium/webdriver/firefox/util'
|
|
6
|
+
require 'selenium/webdriver/firefox/extension'
|
|
7
|
+
require 'selenium/webdriver/firefox/socket_lock'
|
|
8
|
+
require 'selenium/webdriver/firefox/binary'
|
|
9
|
+
require 'selenium/webdriver/firefox/profiles_ini'
|
|
10
|
+
require 'selenium/webdriver/firefox/profile'
|
|
11
|
+
require 'selenium/webdriver/firefox/launcher'
|
|
12
|
+
require 'selenium/webdriver/firefox/bridge'
|
|
13
|
+
|
|
14
|
+
module Selenium
|
|
15
|
+
module WebDriver
|
|
16
|
+
module Firefox
|
|
17
|
+
|
|
18
|
+
DEFAULT_PORT = 7055
|
|
19
|
+
DEFAULT_ENABLE_NATIVE_EVENTS = Platform.os == :windows
|
|
20
|
+
DEFAULT_SECURE_SSL = false
|
|
21
|
+
DEFAULT_ASSUME_UNTRUSTED_ISSUER = true
|
|
22
|
+
DEFAULT_LOAD_NO_FOCUS_LIB = false
|
|
23
|
+
|
|
24
|
+
def self.path=(path)
|
|
25
|
+
Binary.path = path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# SocketError was added in Ruby 1.8.7.
|
|
34
|
+
# If it's not defined, we add it here so it can be used in rescues.
|
|
35
|
+
unless defined? SocketError
|
|
36
|
+
class SocketError < IOError; end
|
|
37
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Selenium
|
|
2
|
+
module WebDriver
|
|
3
|
+
module Firefox
|
|
4
|
+
|
|
5
|
+
# @api private
|
|
6
|
+
class Binary
|
|
7
|
+
|
|
8
|
+
NO_FOCUS_LIBRARY_NAME = "x_ignore_nofocus.so"
|
|
9
|
+
NO_FOCUS_LIBRARIES = [
|
|
10
|
+
["#{WebDriver.root}/selenium/webdriver/firefox/native/linux/amd64/#{NO_FOCUS_LIBRARY_NAME}", "amd64/#{NO_FOCUS_LIBRARY_NAME}"],
|
|
11
|
+
["#{WebDriver.root}/selenium/webdriver/firefox/native/linux/x86/#{NO_FOCUS_LIBRARY_NAME}", "x86/#{NO_FOCUS_LIBRARY_NAME}"],
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
WAIT_TIMEOUT = 90
|
|
15
|
+
QUIT_TIMEOUT = 5
|
|
16
|
+
|
|
17
|
+
def start_with(profile, profile_path, *args)
|
|
18
|
+
profile_path = profile_path.gsub("/", "\\") if Platform.windows?
|
|
19
|
+
|
|
20
|
+
ENV['XRE_CONSOLE_LOG'] = profile.log_file if profile.log_file
|
|
21
|
+
ENV['XRE_PROFILE_PATH'] = profile_path
|
|
22
|
+
ENV['MOZ_NO_REMOTE'] = '1' # able to launch multiple instances
|
|
23
|
+
ENV['MOZ_CRASHREPORTER_DISABLE'] = '1' # disable breakpad
|
|
24
|
+
ENV['NO_EM_RESTART'] = '1' # prevent the binary from detaching from the console
|
|
25
|
+
|
|
26
|
+
if Platform.linux? && (profile.native_events? || profile.load_no_focus_lib?)
|
|
27
|
+
modify_link_library_path profile_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
execute(*args)
|
|
31
|
+
cope_with_mac_strangeness(args) if Platform.mac?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def quit
|
|
35
|
+
return unless @process
|
|
36
|
+
@process.poll_for_exit QUIT_TIMEOUT
|
|
37
|
+
rescue ChildProcess::TimeoutError
|
|
38
|
+
# ok, force quit
|
|
39
|
+
@process.stop QUIT_TIMEOUT
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def wait
|
|
43
|
+
@process.poll_for_exit(WAIT_TIMEOUT) if @process
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def execute(*extra_args)
|
|
49
|
+
args = [self.class.path, "-no-remote"] + extra_args
|
|
50
|
+
@process = ChildProcess.build(*args)
|
|
51
|
+
@process.io.inherit! if $DEBUG
|
|
52
|
+
@process.start
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def cope_with_mac_strangeness(args)
|
|
56
|
+
sleep 0.3
|
|
57
|
+
|
|
58
|
+
if @process.crashed?
|
|
59
|
+
# ok, trying a restart
|
|
60
|
+
sleep 7
|
|
61
|
+
execute(*args)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ensure we're ok
|
|
65
|
+
sleep 0.3
|
|
66
|
+
if @process.crashed?
|
|
67
|
+
raise Error::WebDriverError, "unable to start Firefox cleanly, args: #{args.inspect}"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def modify_link_library_path(profile_path)
|
|
72
|
+
paths = []
|
|
73
|
+
|
|
74
|
+
NO_FOCUS_LIBRARIES.each do |from, to|
|
|
75
|
+
dest = File.join(profile_path, to)
|
|
76
|
+
FileUtils.mkdir_p File.dirname(dest)
|
|
77
|
+
FileUtils.cp from, dest
|
|
78
|
+
|
|
79
|
+
paths << File.expand_path(File.dirname(dest))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
paths += ENV['LD_LIBRARY_PATH'].to_s.split(File::PATH_SEPARATOR)
|
|
83
|
+
|
|
84
|
+
ENV['LD_LIBRARY_PATH'] = paths.uniq.join(File::PATH_SEPARATOR)
|
|
85
|
+
ENV['LD_PRELOAD'] = NO_FOCUS_LIBRARY_NAME
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
# @api private
|
|
92
|
+
#
|
|
93
|
+
# @see Firefox.path=
|
|
94
|
+
#
|
|
95
|
+
|
|
96
|
+
def path=(path)
|
|
97
|
+
Platform.assert_executable(path)
|
|
98
|
+
@path = path
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def path
|
|
102
|
+
@path ||= case Platform.os
|
|
103
|
+
when :macosx
|
|
104
|
+
macosx_path
|
|
105
|
+
when :windows
|
|
106
|
+
windows_path
|
|
107
|
+
when :linux, :unix
|
|
108
|
+
Platform.find_binary("firefox3", "firefox2", "firefox") || "/usr/bin/firefox"
|
|
109
|
+
else
|
|
110
|
+
raise Error::WebDriverError, "unknown platform: #{Platform.os}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
@path = Platform.cygwin_path(@path) if Platform.cygwin?
|
|
114
|
+
|
|
115
|
+
unless File.file?(@path.to_s)
|
|
116
|
+
raise Error::WebDriverError, "Could not find Firefox binary (os=#{Platform.os}). Make sure Firefox is installed or set the path manually with #{self}.path="
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
@path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def windows_path
|
|
125
|
+
windows_registry_path || likely_windows_path || Platform.find_binary("firefox")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def macosx_path
|
|
129
|
+
path = "/Applications/Firefox.app/Contents/MacOS/firefox-bin"
|
|
130
|
+
path = Platform.find_binary("firefox-bin") unless File.exist?(path)
|
|
131
|
+
|
|
132
|
+
path
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def windows_registry_path
|
|
136
|
+
require 'win32/registry'
|
|
137
|
+
|
|
138
|
+
lm = Win32::Registry::HKEY_LOCAL_MACHINE
|
|
139
|
+
lm.open("SOFTWARE\\Mozilla\\Mozilla Firefox") do |reg|
|
|
140
|
+
main = lm.open("SOFTWARE\\Mozilla\\Mozilla Firefox\\#{reg.keys[0]}\\Main")
|
|
141
|
+
if entry = main.find { |key, type, data| key =~ /pathtoexe/i }
|
|
142
|
+
return entry.last
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
rescue LoadError
|
|
146
|
+
# older JRuby or IronRuby does not have win32/registry
|
|
147
|
+
rescue Win32::Registry::Error
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def likely_windows_path
|
|
151
|
+
[
|
|
152
|
+
"#{ ENV['PROGRAMFILES'] || "\\Program Files" }\\Mozilla Firefox\\firefox.exe",
|
|
153
|
+
"#{ ENV['ProgramFiles(x86)'] || "\\Program Files (x86)" }\\Mozilla Firefox\\firefox.exe"
|
|
154
|
+
].find { |path| File.executable?(path) }
|
|
155
|
+
end
|
|
156
|
+
end # class << self
|
|
157
|
+
|
|
158
|
+
end # Binary
|
|
159
|
+
end # Firefox
|
|
160
|
+
end # WebDriver
|
|
161
|
+
end # Selenium
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Selenium
|
|
2
|
+
module WebDriver
|
|
3
|
+
module Firefox
|
|
4
|
+
|
|
5
|
+
# @api private
|
|
6
|
+
class Bridge < Remote::Bridge
|
|
7
|
+
|
|
8
|
+
def initialize(opts = {})
|
|
9
|
+
port = opts.delete(:port) || DEFAULT_PORT
|
|
10
|
+
profile = opts.delete(:profile)
|
|
11
|
+
http_client = opts.delete(:http_client)
|
|
12
|
+
|
|
13
|
+
@launcher = create_launcher(port, profile)
|
|
14
|
+
|
|
15
|
+
unless opts.empty?
|
|
16
|
+
raise ArgumentError, "unknown option#{'s' if opts.size != 1}: #{opts.inspect}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@launcher.launch
|
|
20
|
+
|
|
21
|
+
remote_opts = {
|
|
22
|
+
:url => @launcher.url,
|
|
23
|
+
:desired_capabilities => Remote::Capabilities.firefox(:native_events => DEFAULT_ENABLE_NATIVE_EVENTS)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
remote_opts.merge!(:http_client => http_client) if http_client
|
|
27
|
+
|
|
28
|
+
super(remote_opts)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def browser
|
|
32
|
+
:firefox
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def driver_extensions
|
|
36
|
+
[
|
|
37
|
+
DriverExtensions::TakesScreenshot,
|
|
38
|
+
DriverExtensions::HasInputDevices
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def quit
|
|
43
|
+
super
|
|
44
|
+
@launcher.quit
|
|
45
|
+
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def create_launcher(port, profile)
|
|
52
|
+
Launcher.new Binary.new, port, profile
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
end # Bridge
|
|
56
|
+
end # Firefox
|
|
57
|
+
end # WebDriver
|
|
58
|
+
end # Selenium
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Selenium
|
|
2
|
+
module WebDriver
|
|
3
|
+
module Firefox
|
|
4
|
+
|
|
5
|
+
# @api private
|
|
6
|
+
class Extension
|
|
7
|
+
def initialize(path)
|
|
8
|
+
unless File.exist?(path)
|
|
9
|
+
raise Error::WebDriverError, "could not find extension at #{path.inspect}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
@path = path
|
|
13
|
+
@should_reap_root = false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write_to(extensions_dir)
|
|
17
|
+
root_dir = create_root
|
|
18
|
+
ext_path = File.join extensions_dir, read_id_from_install_rdf(root_dir)
|
|
19
|
+
|
|
20
|
+
FileUtils.rm_rf ext_path
|
|
21
|
+
FileUtils.mkdir_p File.dirname(ext_path), :mode => 0700
|
|
22
|
+
FileUtils.cp_r root_dir, ext_path
|
|
23
|
+
|
|
24
|
+
FileReaper.reap(root_dir) if @should_reap_root
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def create_root
|
|
30
|
+
if File.directory? @path
|
|
31
|
+
@path
|
|
32
|
+
else
|
|
33
|
+
unless Zipper::EXTENSIONS.include? File.extname(@path)
|
|
34
|
+
raise Error::WebDriverError, "expected #{Zipper::EXTENSIONS.join(" or ")}, got #{@path.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@should_reap_root = true
|
|
38
|
+
Zipper.unzip(@path)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_id_from_install_rdf(directory)
|
|
43
|
+
rdf_path = File.join(directory, "install.rdf")
|
|
44
|
+
doc = REXML::Document.new(File.read(rdf_path))
|
|
45
|
+
|
|
46
|
+
REXML::XPath.first(doc, "//em:id").text
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end # Extension
|
|
50
|
+
end # Firefox
|
|
51
|
+
end # WebDriver
|
|
52
|
+
end # Selenium
|
|
Binary file
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module Selenium
|
|
2
|
+
module WebDriver
|
|
3
|
+
module Firefox
|
|
4
|
+
|
|
5
|
+
# @api private
|
|
6
|
+
class Launcher
|
|
7
|
+
|
|
8
|
+
SOCKET_LOCK_TIMEOUT = 45
|
|
9
|
+
STABLE_CONNECTION_TIMEOUT = 60
|
|
10
|
+
|
|
11
|
+
def initialize(binary, port, profile = nil)
|
|
12
|
+
@binary = binary
|
|
13
|
+
@port = Integer(port)
|
|
14
|
+
|
|
15
|
+
raise Error::WebDriverError, "invalid port: #{@port}" if @port < 1
|
|
16
|
+
|
|
17
|
+
if profile.kind_of? Profile
|
|
18
|
+
@profile = profile
|
|
19
|
+
else
|
|
20
|
+
@profile_name = profile
|
|
21
|
+
@profile = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@host = "127.0.0.1"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def url
|
|
28
|
+
"http://#{@host}:#{@port}/hub"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def launch
|
|
32
|
+
socket_lock.locked do
|
|
33
|
+
find_free_port
|
|
34
|
+
create_profile
|
|
35
|
+
start_silent_and_wait
|
|
36
|
+
start
|
|
37
|
+
connect_until_stable
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def quit
|
|
44
|
+
@binary.quit
|
|
45
|
+
FileReaper.reap(@profile_dir) if @profile_dir
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find_free_port
|
|
49
|
+
@port = PortProber.above @port
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def create_profile
|
|
53
|
+
fetch_profile unless @profile
|
|
54
|
+
|
|
55
|
+
@profile.add_webdriver_extension
|
|
56
|
+
@profile.port = @port
|
|
57
|
+
@profile_dir = @profile.layout_on_disk
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def start
|
|
61
|
+
assert_profile
|
|
62
|
+
@binary.start_with @profile, @profile_dir, "-foreground"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def start_silent_and_wait
|
|
66
|
+
assert_profile
|
|
67
|
+
|
|
68
|
+
@binary.start_with @profile, @profile_dir, "-silent"
|
|
69
|
+
@binary.wait
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def connect_until_stable
|
|
73
|
+
poller = SocketPoller.new(@host, @port, STABLE_CONNECTION_TIMEOUT)
|
|
74
|
+
|
|
75
|
+
unless poller.connected?
|
|
76
|
+
@binary.quit
|
|
77
|
+
raise Error::WebDriverError, "unable to obtain stable firefox connection in #{STABLE_CONNECTION_TIMEOUT} seconds (#{@host}:#{@port})"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def fetch_profile
|
|
82
|
+
if @profile_name
|
|
83
|
+
@profile = Profile.from_name @profile_name
|
|
84
|
+
|
|
85
|
+
unless @profile
|
|
86
|
+
raise Error::WebDriverError, "unable to find profile named: #{@profile_name.inspect}"
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
@profile = Profile.new
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def assert_profile
|
|
94
|
+
raise Error::WebDriverError, "must create_profile first" unless @profile && @profile_dir
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def socket_lock
|
|
98
|
+
@socket_lock ||= SocketLock.new(@port - 1, SOCKET_LOCK_TIMEOUT)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
end # Launcher
|
|
102
|
+
end # Firefox
|
|
103
|
+
end # WebDriver
|
|
104
|
+
end # Selenium
|
|
Binary file
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
module Selenium
|
|
2
|
+
module WebDriver
|
|
3
|
+
module Firefox
|
|
4
|
+
class Profile
|
|
5
|
+
include ProfileHelper
|
|
6
|
+
|
|
7
|
+
WEBDRIVER_EXTENSION_PATH = File.expand_path("#{WebDriver.root}/selenium/webdriver/firefox/extension/webdriver.xpi")
|
|
8
|
+
WEBDRIVER_PREFS = {
|
|
9
|
+
:native_events => 'webdriver_enable_native_events',
|
|
10
|
+
:untrusted_certs => 'webdriver_accept_untrusted_certs',
|
|
11
|
+
:untrusted_issuer => 'webdriver_assume_untrusted_issuer',
|
|
12
|
+
:port => 'webdriver_firefox_port',
|
|
13
|
+
:log_file => 'webdriver.log.file'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
attr_reader :name, :log_file
|
|
17
|
+
attr_writer :secure_ssl, :native_events, :load_no_focus_lib
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def ini
|
|
21
|
+
@ini ||= ProfilesIni.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def from_name(name)
|
|
25
|
+
ini[name]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#
|
|
30
|
+
# Create a new Profile instance
|
|
31
|
+
#
|
|
32
|
+
# @example User configured profile
|
|
33
|
+
#
|
|
34
|
+
# profile = Selenium::WebDriver::Firefox::Profile.new
|
|
35
|
+
# profile['network.proxy.http'] = 'localhost'
|
|
36
|
+
# profile['network.proxy.http_port'] = 9090
|
|
37
|
+
#
|
|
38
|
+
# driver = Selenium::WebDriver.for :firefox, :profile => profile
|
|
39
|
+
#
|
|
40
|
+
|
|
41
|
+
def initialize(model = nil)
|
|
42
|
+
@model = verify_model(model)
|
|
43
|
+
|
|
44
|
+
model_prefs = read_model_prefs
|
|
45
|
+
|
|
46
|
+
if model_prefs.empty?
|
|
47
|
+
@native_events = DEFAULT_ENABLE_NATIVE_EVENTS
|
|
48
|
+
@secure_ssl = DEFAULT_SECURE_SSL
|
|
49
|
+
@untrusted_issuer = DEFAULT_ASSUME_UNTRUSTED_ISSUER
|
|
50
|
+
@load_no_focus_lib = DEFAULT_LOAD_NO_FOCUS_LIB
|
|
51
|
+
|
|
52
|
+
@additional_prefs = {}
|
|
53
|
+
else
|
|
54
|
+
# TODO: clean this up
|
|
55
|
+
@native_events = model_prefs.delete(WEBDRIVER_PREFS[:native_events]) == "true"
|
|
56
|
+
@secure_ssl = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_certs]) != "true"
|
|
57
|
+
@untrusted_issuer = model_prefs.delete(WEBDRIVER_PREFS[:untrusted_issuer]) == "true"
|
|
58
|
+
@load_no_focus_lib = model_prefs.delete(WEBDRIVER_PREFS[:load_no_focus_lib]) == "true" # not stored in profile atm, so will always be false.
|
|
59
|
+
@additional_prefs = model_prefs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@extensions = {}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def layout_on_disk
|
|
66
|
+
profile_dir = @model ? create_tmp_copy(@model) : Dir.mktmpdir("webdriver-profile")
|
|
67
|
+
FileReaper << profile_dir
|
|
68
|
+
|
|
69
|
+
install_extensions(profile_dir)
|
|
70
|
+
delete_lock_files(profile_dir)
|
|
71
|
+
delete_extensions_cache(profile_dir)
|
|
72
|
+
update_user_prefs_in(profile_dir)
|
|
73
|
+
|
|
74
|
+
profile_dir
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
#
|
|
79
|
+
# Set a preference for this particular profile.
|
|
80
|
+
#
|
|
81
|
+
# @see http://kb.mozillazine.org/About:config_entries
|
|
82
|
+
# @see http://preferential.mozdev.org/preferences.html
|
|
83
|
+
#
|
|
84
|
+
|
|
85
|
+
def []=(key, value)
|
|
86
|
+
case value
|
|
87
|
+
when String
|
|
88
|
+
if Util.stringified?(value)
|
|
89
|
+
raise ArgumentError, "preference values must be plain strings: #{key.inspect} => #{value.inspect}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
value = MultiJson.encode(value)
|
|
93
|
+
when TrueClass, FalseClass, Integer, Float
|
|
94
|
+
value = value.to_s
|
|
95
|
+
else
|
|
96
|
+
raise TypeError, "invalid preference: #{value.inspect}:#{value.class}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@additional_prefs[key.to_s] = value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def port=(port)
|
|
103
|
+
self[WEBDRIVER_PREFS[:port]] = port
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def log_file=(file)
|
|
107
|
+
@log_file = file
|
|
108
|
+
self[WEBDRIVER_PREFS[:log_file]] = file
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def add_webdriver_extension
|
|
112
|
+
unless @extensions.has_key?(:webdriver)
|
|
113
|
+
add_extension(WEBDRIVER_EXTENSION_PATH, :webdriver)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#
|
|
118
|
+
# Add the extension (directory, .zip or .xpi) at the given path to the profile.
|
|
119
|
+
#
|
|
120
|
+
|
|
121
|
+
def add_extension(path, name = extension_name_for(path))
|
|
122
|
+
@extensions[name] = Extension.new(path)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def native_events?
|
|
126
|
+
@native_events == true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def load_no_focus_lib?
|
|
130
|
+
@load_no_focus_lib == true
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def secure_ssl?
|
|
134
|
+
@secure_ssl == true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def assume_untrusted_certificate_issuer?
|
|
138
|
+
@untrusted_issuer == true
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def assume_untrusted_certificate_issuer=(bool)
|
|
142
|
+
@untrusted_issuer = bool
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def proxy=(proxy)
|
|
146
|
+
unless proxy.kind_of? Proxy
|
|
147
|
+
raise TypeError, "expected #{Proxy.name}, got #{proxy.inspect}:#{proxy.class}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
case proxy.type
|
|
151
|
+
when :manual
|
|
152
|
+
self['network.proxy.type'] = 1
|
|
153
|
+
|
|
154
|
+
set_manual_proxy_preference "ftp", proxy.ftp
|
|
155
|
+
set_manual_proxy_preference "http", proxy.http
|
|
156
|
+
set_manual_proxy_preference "ssl", proxy.ssl
|
|
157
|
+
|
|
158
|
+
if proxy.no_proxy
|
|
159
|
+
self["network.proxy.no_proxies_on"] = proxy.no_proxy
|
|
160
|
+
else
|
|
161
|
+
self["network.proxy.no_proxies_on"] = ""
|
|
162
|
+
end
|
|
163
|
+
when :pac
|
|
164
|
+
self['network.proxy.type'] = 2
|
|
165
|
+
self['network.proxy.autoconfig_url'] = proxy.pac
|
|
166
|
+
when :auto_detect
|
|
167
|
+
self['network.proxy.type'] = 4
|
|
168
|
+
else
|
|
169
|
+
raise ArgumentError, "unsupported proxy type #{proxy.type}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
proxy
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def set_manual_proxy_preference(key, value)
|
|
178
|
+
return unless value
|
|
179
|
+
|
|
180
|
+
host, port = value.to_s.split(":", 2)
|
|
181
|
+
|
|
182
|
+
self["network.proxy.#{key}"] = host
|
|
183
|
+
self["network.proxy.#{key}_port"] = Integer(port) if port
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def install_extensions(directory)
|
|
187
|
+
destination = File.join(directory, "extensions")
|
|
188
|
+
|
|
189
|
+
@extensions.each do |name, extension|
|
|
190
|
+
p :extension => name if $DEBUG
|
|
191
|
+
extension.write_to(destination)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def read_model_prefs
|
|
196
|
+
return {} unless @model
|
|
197
|
+
|
|
198
|
+
read_user_prefs(File.join(@model, 'user.js'))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def delete_extensions_cache(directory)
|
|
202
|
+
FileUtils.rm_f File.join(directory, "extensions.cache")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def delete_lock_files(directory)
|
|
206
|
+
%w[.parentlock parent.lock].each do |name|
|
|
207
|
+
FileUtils.rm_f File.join(directory, name)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def extension_name_for(path)
|
|
212
|
+
File.basename(path, File.extname(path))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def update_user_prefs_in(directory)
|
|
216
|
+
path = File.join(directory, 'user.js')
|
|
217
|
+
prefs = read_user_prefs(path)
|
|
218
|
+
|
|
219
|
+
prefs.merge! DEFAULT_PREFERENCES
|
|
220
|
+
prefs.merge! @additional_prefs
|
|
221
|
+
prefs.merge! FROZEN_PREFERENCES
|
|
222
|
+
|
|
223
|
+
prefs[WEBDRIVER_PREFS[:untrusted_certs]] = !secure_ssl?
|
|
224
|
+
prefs[WEBDRIVER_PREFS[:native_events]] = native_events?
|
|
225
|
+
prefs[WEBDRIVER_PREFS[:untrusted_issuer]] = assume_untrusted_certificate_issuer?
|
|
226
|
+
|
|
227
|
+
# If the user sets the home page, we should also start up there
|
|
228
|
+
prefs["startup.homepage_welcome_url"] = prefs["browser.startup.homepage"]
|
|
229
|
+
|
|
230
|
+
write_prefs prefs, path
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def read_user_prefs(path)
|
|
234
|
+
return {} unless File.exist?(path)
|
|
235
|
+
|
|
236
|
+
prefs = {}
|
|
237
|
+
|
|
238
|
+
File.read(path).split("\n").each do |line|
|
|
239
|
+
if line =~ /user_pref\("([^"]+)"\s*,\s*(.+?)\);/
|
|
240
|
+
prefs[$1.strip] = $2.strip
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
prefs
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def write_prefs(prefs, path)
|
|
248
|
+
File.open(path, "w") { |file|
|
|
249
|
+
prefs.each do |key, value|
|
|
250
|
+
p key => value if $DEBUG
|
|
251
|
+
file.puts %{user_pref("#{key}", #{value});}
|
|
252
|
+
end
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
DEFAULT_PREFERENCES = {
|
|
257
|
+
"browser.startup.page" => '0',
|
|
258
|
+
"browser.startup.homepage" => '"about:blank"',
|
|
259
|
+
"dom.max_script_run_time" => '30',
|
|
260
|
+
}.freeze
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# Profile preferences that are essential to the Firefox driver operating
|
|
264
|
+
# correctly. Users are not permitted to override these values.
|
|
265
|
+
|
|
266
|
+
FROZEN_PREFERENCES = {
|
|
267
|
+
"app.update.auto" => 'false',
|
|
268
|
+
"app.update.enabled" => 'false',
|
|
269
|
+
"browser.download.manager.showWhenStarting" => 'false',
|
|
270
|
+
"browser.EULA.override" => 'true',
|
|
271
|
+
"browser.EULA.3.accepted" => 'true',
|
|
272
|
+
"browser.link.open_external" => '2',
|
|
273
|
+
"browser.link.open_newwindow" => '2',
|
|
274
|
+
"browser.safebrowsing.enabled" => 'false',
|
|
275
|
+
"browser.safebrowsing.malware.enabled" => 'false',
|
|
276
|
+
"browser.search.update" => 'false',
|
|
277
|
+
"browser.sessionstore.resume_from_crash" => 'false',
|
|
278
|
+
"browser.shell.checkDefaultBrowser" => 'false',
|
|
279
|
+
"browser.tabs.warnOnClose" => 'false',
|
|
280
|
+
"browser.tabs.warnOnOpen" => 'false',
|
|
281
|
+
"devtools.errorconsole.enabled" => 'true',
|
|
282
|
+
"dom.disable_open_during_load" => 'false',
|
|
283
|
+
"extensions.autoDisableScopes" => '10',
|
|
284
|
+
"extensions.logging.enabled" => 'true',
|
|
285
|
+
"extensions.update.enabled" => 'false',
|
|
286
|
+
"extensions.update.notifyUser" => 'false',
|
|
287
|
+
"network.manage-offline-status" => 'false',
|
|
288
|
+
"network.http.phishy-userpass-length" => '255',
|
|
289
|
+
"network.http.max-connections-per-server" => '10',
|
|
290
|
+
"offline-apps.allow_by_default" => 'true',
|
|
291
|
+
"prompts.tab_modal.enabled" => "false",
|
|
292
|
+
"security.warn_entering_secure" => 'false',
|
|
293
|
+
"security.warn_submit_insecure" => 'false',
|
|
294
|
+
"security.warn_entering_secure.show_once" => 'false',
|
|
295
|
+
"security.warn_entering_weak" => 'false',
|
|
296
|
+
"security.warn_entering_weak.show_once" => 'false',
|
|
297
|
+
"security.warn_leaving_secure" => 'false',
|
|
298
|
+
"security.warn_leaving_secure.show_once" => 'false',
|
|
299
|
+
"security.warn_submit_insecure" => 'false',
|
|
300
|
+
"security.warn_viewing_mixed" => 'false',
|
|
301
|
+
"security.warn_viewing_mixed.show_once" => 'false',
|
|
302
|
+
"signon.rememberSignons" => 'false',
|
|
303
|
+
"toolkit.networkmanager.disable" => 'true',
|
|
304
|
+
"toolkit.telemetry.prompted" => '2',
|
|
305
|
+
"toolkit.telemetry.enabled" => 'false',
|
|
306
|
+
"toolkit.telemetry.rejected" => 'true',
|
|
307
|
+
"javascript.options.showInConsole" => 'true',
|
|
308
|
+
"browser.dom.window.dump.enabled" => 'true',
|
|
309
|
+
"dom.report_all_js_exceptions" => "true"
|
|
310
|
+
}.freeze
|
|
311
|
+
|
|
312
|
+
end # Profile
|
|
313
|
+
end # Firefox
|
|
314
|
+
end # WebDriver
|
|
315
|
+
end # Selenium
|