chromate-rb 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +39 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +124 -0
- data/Rakefile +71 -0
- data/dockerfiles/Dockerfile +85 -0
- data/dockerfiles/docker-entrypoint.sh +15 -0
- data/docs/browser.md +163 -0
- data/lib/chromate/actions/dom.rb +39 -0
- data/lib/chromate/actions/navigate.rb +71 -0
- data/lib/chromate/actions/screenshot.rb +56 -0
- data/lib/chromate/browser.rb +113 -0
- data/lib/chromate/c_logger.rb +22 -0
- data/lib/chromate/client.rb +120 -0
- data/lib/chromate/configuration.rb +119 -0
- data/lib/chromate/element.rb +194 -0
- data/lib/chromate/elements/select.rb +19 -0
- data/lib/chromate/exceptions.rb +9 -0
- data/lib/chromate/hardwares/mouse_controller.rb +67 -0
- data/lib/chromate/hardwares/mouses/linux_controller.rb +79 -0
- data/lib/chromate/hardwares/mouses/mac_os_controller.rb +133 -0
- data/lib/chromate/hardwares/mouses/virtual_controller.rb +81 -0
- data/lib/chromate/hardwares.rb +33 -0
- data/lib/chromate/helpers.rb +23 -0
- data/lib/chromate/user_agent.rb +34 -0
- data/lib/chromate/version.rb +5 -0
- data/lib/chromate.rb +17 -0
- data/results/brotector.png +0 -0
- data/results/cloudflare.png +0 -0
- data/sig/chromate.rbs +4 -0
- metadata +111 -0
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Actions
|
5
|
+
module Dom
|
6
|
+
def find_element(selector)
|
7
|
+
Chromate::Element.new(selector, @client)
|
8
|
+
end
|
9
|
+
|
10
|
+
def click_element(selector)
|
11
|
+
find_element(selector).click
|
12
|
+
end
|
13
|
+
|
14
|
+
def hover_element(selector)
|
15
|
+
find_element(selector).hover
|
16
|
+
end
|
17
|
+
|
18
|
+
def type_text(selector, text)
|
19
|
+
find_element(selector).type(text)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_text(selector)
|
23
|
+
find_element(selector).text
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_property(selector, property)
|
27
|
+
find_element(selector).attributes[property]
|
28
|
+
end
|
29
|
+
|
30
|
+
def select_option(selector, option)
|
31
|
+
Chromate::Elements::Select.new(selector, @client).select_option(option)
|
32
|
+
end
|
33
|
+
|
34
|
+
def evaluate_script(script)
|
35
|
+
@client.send_message('Runtime.evaluate', expression: script)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Actions
|
5
|
+
module Navigate
|
6
|
+
# @param [String] url
|
7
|
+
# @return [self]
|
8
|
+
def navigate_to(url)
|
9
|
+
@client.send_message('Page.enable')
|
10
|
+
@client.send_message('Page.navigate', url: url)
|
11
|
+
wait_for_page_load
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [self]
|
15
|
+
def wait_for_page_load # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
16
|
+
page_loaded = false
|
17
|
+
dom_content_loaded = false
|
18
|
+
frame_stopped_loading = false
|
19
|
+
|
20
|
+
# Utiliser un Mutex pour la synchronisation
|
21
|
+
mutex = Mutex.new
|
22
|
+
condition = ConditionVariable.new
|
23
|
+
|
24
|
+
# S'abonner aux messages WebSocket
|
25
|
+
listener = proc do |message|
|
26
|
+
mutex.synchronize do
|
27
|
+
case message['method']
|
28
|
+
when 'Page.domContentEventFired'
|
29
|
+
dom_content_loaded = true
|
30
|
+
condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
|
31
|
+
when 'Page.loadEventFired'
|
32
|
+
page_loaded = true
|
33
|
+
condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
|
34
|
+
when 'Page.frameStoppedLoading'
|
35
|
+
frame_stopped_loading = true
|
36
|
+
condition.signal if dom_content_loaded && page_loaded && frame_stopped_loading
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@client.on_message(&listener)
|
42
|
+
|
43
|
+
# Attendre les trois événements (DOMContent, Load et FrameStoppedLoading) avec un timeout
|
44
|
+
Timeout.timeout(15) do
|
45
|
+
mutex.synchronize do
|
46
|
+
condition.wait(mutex) until dom_content_loaded && page_loaded && frame_stopped_loading
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Nettoyer l'écouteur WebSocket
|
51
|
+
@client.on_message { |msg| } # Supprime tous les anciens écouteurs en ajoutant un listener vide
|
52
|
+
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [self]
|
57
|
+
def refresh
|
58
|
+
@client.send_message('Page.reload')
|
59
|
+
wait_for_page_load
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [self]
|
64
|
+
def go_back
|
65
|
+
@client.send_message('Page.goBack')
|
66
|
+
wait_for_page_load
|
67
|
+
self
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chromate
|
4
|
+
module Actions
|
5
|
+
module Screenshot
|
6
|
+
def screenshot_to_file(file_path, options = {})
|
7
|
+
image_data = screenshot(options)
|
8
|
+
File.binwrite(file_path, image_data)
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def screenshot_full_page(file_path, options = {})
|
13
|
+
metrics = @client.send_message('Page.getLayoutMetrics')
|
14
|
+
|
15
|
+
content_size = metrics['contentSize']
|
16
|
+
width = content_size['width'].ceil
|
17
|
+
height = content_size['height'].ceil
|
18
|
+
|
19
|
+
@client.send_message('Emulation.setDeviceMetricsOverride', {
|
20
|
+
mobile: false,
|
21
|
+
width: width,
|
22
|
+
height: height,
|
23
|
+
deviceScaleFactor: 1
|
24
|
+
})
|
25
|
+
|
26
|
+
screenshot_to_file(file_path, options)
|
27
|
+
|
28
|
+
@client.send_message('Emulation.clearDeviceMetricsOverride')
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def xvfb_screenshot(file_path)
|
33
|
+
display = ENV['DISPLAY'] || ':99'
|
34
|
+
system("xwd -root -display #{display} | convert xwd:- #{file_path}")
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def screenshot(options = {})
|
40
|
+
default_options = {
|
41
|
+
format: 'png',
|
42
|
+
fromSurface: true
|
43
|
+
}
|
44
|
+
|
45
|
+
params = default_options.merge(options)
|
46
|
+
|
47
|
+
@client.send_message('Page.enable')
|
48
|
+
|
49
|
+
result = @client.send_message('Page.captureScreenshot', params)
|
50
|
+
|
51
|
+
image_data = result['data']
|
52
|
+
Base64.decode64(image_data)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'net/http'
|
7
|
+
require 'websocket-client-simple'
|
8
|
+
require_relative 'helpers'
|
9
|
+
require_relative 'client'
|
10
|
+
require_relative 'element'
|
11
|
+
require_relative 'hardwares'
|
12
|
+
require_relative 'elements/select'
|
13
|
+
require_relative 'user_agent'
|
14
|
+
require_relative 'actions/navigate'
|
15
|
+
require_relative 'actions/screenshot'
|
16
|
+
require_relative 'actions/dom'
|
17
|
+
|
18
|
+
module Chromate
|
19
|
+
class Browser
|
20
|
+
attr_reader :client, :options
|
21
|
+
|
22
|
+
include Helpers
|
23
|
+
include Actions::Navigate
|
24
|
+
include Actions::Screenshot
|
25
|
+
include Actions::Dom
|
26
|
+
|
27
|
+
def initialize(options = {})
|
28
|
+
@options = config.options.merge(options)
|
29
|
+
@chrome_path = @options.fetch(:chrome_path)
|
30
|
+
@user_data_dir = @options.fetch(:user_data_dir, "/tmp/chromate_#{SecureRandom.hex}")
|
31
|
+
@headless = @options.fetch(:headless)
|
32
|
+
@xfvb = @options.fetch(:xfvb)
|
33
|
+
@native_control = @options.fetch(:native_control)
|
34
|
+
@record = @options.fetch(:record, false)
|
35
|
+
@process = nil
|
36
|
+
@xfvb_process = nil
|
37
|
+
@record_process = nil
|
38
|
+
@client = nil
|
39
|
+
@args = [
|
40
|
+
@chrome_path,
|
41
|
+
"--user-data-dir=#{@user_data_dir}",
|
42
|
+
"--lang=#{@options[:lang] || "fr-FR"}"
|
43
|
+
]
|
44
|
+
|
45
|
+
trap('INT') { stop_and_exit }
|
46
|
+
trap('TERM') { stop_and_exit }
|
47
|
+
|
48
|
+
at_exit { stop }
|
49
|
+
end
|
50
|
+
|
51
|
+
def start
|
52
|
+
build_args
|
53
|
+
@client = Client.new(self)
|
54
|
+
@args << "--remote-debugging-port=#{@client.port}"
|
55
|
+
|
56
|
+
start_video_recording if @record
|
57
|
+
|
58
|
+
if @xfvb
|
59
|
+
if ENV['DISPLAY'].nil?
|
60
|
+
ENV['DISPLAY'] = ':0' if mac? # XQuartz generally uses :0 on Mac
|
61
|
+
ENV['DISPLAY'] = ':99' if linux? # Xvfb generally uses :99 on Linux
|
62
|
+
end
|
63
|
+
@args << "--display=#{ENV.fetch("DISPLAY", nil)}"
|
64
|
+
end
|
65
|
+
|
66
|
+
@process = spawn(*@args, err: 'chrome_errors.log', out: 'chrome_output.log')
|
67
|
+
sleep 2
|
68
|
+
|
69
|
+
@client.start
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def stop
|
74
|
+
Process.kill('TERM', @process) if @process
|
75
|
+
Process.kill('TERM', @record_process) if @record_process
|
76
|
+
Process.kill('TERM', @xfvb_process) if @xfvb_process
|
77
|
+
@client&.stop
|
78
|
+
end
|
79
|
+
|
80
|
+
def native_control?
|
81
|
+
@native_control
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def start_video_recording
|
87
|
+
outfile = File.join(Dir.pwd, "output_video_#{Time.now.to_i}.mp4")
|
88
|
+
@record_process = spawn("ffmpeg -f x11grab -r 25 -s 1920x1080 -i #{ENV.fetch("DISPLAY", ":99")} -pix_fmt yuv420p -y #{outfile}")
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_args
|
92
|
+
exclude_switches = config.exclude_switches || []
|
93
|
+
exclude_switches += @options[:exclude_switches] if @options[:exclude_switches]
|
94
|
+
|
95
|
+
@args += config.generate_arguments(**@options)
|
96
|
+
@args += @options[:options][:args] if @options.dig(:options, :args)
|
97
|
+
@args << "--user-agent=#{@options[:user_agent] || UserAgent.call}"
|
98
|
+
@args << "--exclude-switches=#{exclude_switches.join(",")}" if exclude_switches.any?
|
99
|
+
|
100
|
+
@args
|
101
|
+
end
|
102
|
+
|
103
|
+
def stop_and_exit
|
104
|
+
puts 'Stopping browser...'
|
105
|
+
stop
|
106
|
+
exit
|
107
|
+
end
|
108
|
+
|
109
|
+
def config
|
110
|
+
Chromate.configuration
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Chromate
|
6
|
+
class CLogger < Logger
|
7
|
+
def initialize(logdev, shift_age: 0, shift_size: 1_048_576)
|
8
|
+
super(logdev, shift_age, shift_size)
|
9
|
+
self.formatter = proc do |severity, datetime, _progname, msg|
|
10
|
+
"[Chromate] #{datetime.strftime("%Y-%m-%d %H:%M:%S")} #{severity}: #{msg}\n"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.logger
|
15
|
+
@logger ||= new($stdout)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.log(message, level: :info)
|
19
|
+
logger.send(level, message)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'websocket-client-simple'
|
4
|
+
require 'chromate/helpers'
|
5
|
+
|
6
|
+
module Chromate
|
7
|
+
class Client
|
8
|
+
include Helpers
|
9
|
+
|
10
|
+
def self.listeners
|
11
|
+
@@listeners ||= [] # rubocop:disable Style/ClassVars
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :port, :ws, :browser
|
15
|
+
|
16
|
+
def initialize(browser)
|
17
|
+
@browser = browser
|
18
|
+
options = browser.options
|
19
|
+
@port = options[:port] || find_available_port
|
20
|
+
end
|
21
|
+
|
22
|
+
def start
|
23
|
+
@ws_url = fetch_websocket_debug_url
|
24
|
+
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
25
|
+
@id = 0
|
26
|
+
@callbacks = {}
|
27
|
+
|
28
|
+
client_self = self
|
29
|
+
|
30
|
+
@ws.on :message do |msg|
|
31
|
+
message = JSON.parse(msg.data)
|
32
|
+
client_self.handle_message(message)
|
33
|
+
|
34
|
+
Client.listeners.each do |listener|
|
35
|
+
listener.call(message)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
@ws.on :open do
|
40
|
+
puts "Connexion WebSocket établie avec #{@ws_url}"
|
41
|
+
end
|
42
|
+
|
43
|
+
@ws.on :error do |e|
|
44
|
+
puts "Erreur WebSocket : #{e.message}"
|
45
|
+
end
|
46
|
+
|
47
|
+
@ws.on :close do |_e|
|
48
|
+
puts 'Connexion WebSocket fermée'
|
49
|
+
end
|
50
|
+
|
51
|
+
sleep 0.2
|
52
|
+
client_self.send_message('Target.setDiscoverTargets', { discover: true })
|
53
|
+
client_self
|
54
|
+
end
|
55
|
+
|
56
|
+
def stop
|
57
|
+
@ws&.close
|
58
|
+
end
|
59
|
+
|
60
|
+
def send_message(method, params = {})
|
61
|
+
@id += 1
|
62
|
+
message = { id: @id, method: method, params: params }
|
63
|
+
puts "Envoi du message : #{message}"
|
64
|
+
|
65
|
+
begin
|
66
|
+
@ws.send(message.to_json)
|
67
|
+
@callbacks[@id] = Queue.new
|
68
|
+
result = @callbacks[@id].pop
|
69
|
+
puts "Réponse reçue pour le message #{message[:id]} : #{result}"
|
70
|
+
result
|
71
|
+
rescue StandardError => e
|
72
|
+
puts "Erreur WebSocket lors de l'envoi du message : #{e.message}"
|
73
|
+
reconnect
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def reconnect
|
79
|
+
@ws_url = fetch_websocket_debug_url
|
80
|
+
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
81
|
+
puts 'Reconnexion WebSocket réussie'
|
82
|
+
end
|
83
|
+
|
84
|
+
def handle_message(message)
|
85
|
+
puts "Message reçu : #{message}"
|
86
|
+
return unless message['id'] && @callbacks[message['id']]
|
87
|
+
|
88
|
+
@callbacks[message['id']].push(message['result'])
|
89
|
+
@callbacks.delete(message['id'])
|
90
|
+
end
|
91
|
+
|
92
|
+
# Allowing different parts to subscribe to WebSocket messages
|
93
|
+
def on_message(&block)
|
94
|
+
Client.listeners << block
|
95
|
+
end
|
96
|
+
|
97
|
+
def fetch_websocket_debug_url
|
98
|
+
uri = URI("http://localhost:#{@port}/json/list")
|
99
|
+
response = Net::HTTP.get(uri)
|
100
|
+
targets = JSON.parse(response)
|
101
|
+
|
102
|
+
page_target = targets.find { |target| target['type'] == 'page' }
|
103
|
+
|
104
|
+
if page_target
|
105
|
+
page_target['webSocketDebuggerUrl']
|
106
|
+
else
|
107
|
+
create_new_page_target
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def create_new_page_target
|
112
|
+
# Créer une nouvelle page
|
113
|
+
uri = URI("http://localhost:#{@port}/json/new")
|
114
|
+
response = Net::HTTP.get(uri)
|
115
|
+
new_target = JSON.parse(response)
|
116
|
+
|
117
|
+
new_target['webSocketDebuggerUrl']
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'helpers'
|
4
|
+
require_relative 'exceptions'
|
5
|
+
require_relative 'c_logger'
|
6
|
+
|
7
|
+
module Chromate
|
8
|
+
class Configuration
|
9
|
+
include Helpers
|
10
|
+
include Exceptions
|
11
|
+
DEFAULT_ARGS = [
|
12
|
+
'--no-first-run',
|
13
|
+
'--no-default-browser-check',
|
14
|
+
'--disable-blink-features=AutomationControlled',
|
15
|
+
'--disable-extensions',
|
16
|
+
'--disable-infobars',
|
17
|
+
'--no-sandbox',
|
18
|
+
'--disable-popup-blocking',
|
19
|
+
'--ignore-certificate-errors',
|
20
|
+
'--disable-gpu',
|
21
|
+
'--disable-dev-shm-usage',
|
22
|
+
'--window-size=1920,1080', # TODO: Make this automatic
|
23
|
+
'--hide-crash-restore-bubble'
|
24
|
+
].freeze
|
25
|
+
HEADLESS_ARGS = [
|
26
|
+
'--headless=new',
|
27
|
+
'--window-position=2400,2400'
|
28
|
+
].freeze
|
29
|
+
XVFB_ARGS = [
|
30
|
+
'--window-position=0,0'
|
31
|
+
].freeze
|
32
|
+
DISABLED_FEATURES = %w[
|
33
|
+
Translate
|
34
|
+
OptimizationHints
|
35
|
+
MediaRouter
|
36
|
+
DialMediaRouteProvider
|
37
|
+
CalculateNativeWinOcclusion
|
38
|
+
InterestFeedContentSuggestions
|
39
|
+
CertificateTransparencyComponentUpdater
|
40
|
+
AutofillServerCommunication
|
41
|
+
PrivacySandboxSettings4
|
42
|
+
AutomationControlled
|
43
|
+
].freeze
|
44
|
+
EXCLUDE_SWITCHES = %w[
|
45
|
+
enable-automation
|
46
|
+
].freeze
|
47
|
+
|
48
|
+
attr_accessor :user_data_dir, :headless, :xfvb, :native_control, :args, :headless_args, :xfvb_args, :exclude_switches, :proxy,
|
49
|
+
:disable_features
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@user_data_dir = File.expand_path('~/.config/google-chrome/Default')
|
53
|
+
@headless = true
|
54
|
+
@xfvb = false
|
55
|
+
@native_control = false
|
56
|
+
@proxy = nil
|
57
|
+
@args = [] + DEFAULT_ARGS
|
58
|
+
@headless_args = [] + HEADLESS_ARGS
|
59
|
+
@xfvb_args = [] + XVFB_ARGS
|
60
|
+
@disable_features = [] + DISABLED_FEATURES
|
61
|
+
@exclude_switches = [] + EXCLUDE_SWITCHES
|
62
|
+
|
63
|
+
@args << '--use-angle=metal' if mac?
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.config
|
67
|
+
@config ||= Configuration.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.configure
|
71
|
+
yield(config)
|
72
|
+
end
|
73
|
+
|
74
|
+
def config
|
75
|
+
self.class.config
|
76
|
+
end
|
77
|
+
|
78
|
+
def chrome_path
|
79
|
+
return ENV['CHROME_BIN'] if ENV['CHROME_BIN']
|
80
|
+
|
81
|
+
if mac?
|
82
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
83
|
+
elsif linux?
|
84
|
+
'/usr/bin/google-chrome-stable'
|
85
|
+
elsif windows?
|
86
|
+
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
87
|
+
else
|
88
|
+
raise Exceptions::InvalidPlatformError, 'Unsupported platform'
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_arguments(headless: @headless, xfvb: @xfvb, proxy: @proxy, disable_features: @disable_features, **_args)
|
93
|
+
dynamic_args = []
|
94
|
+
|
95
|
+
dynamic_args += @headless_args if headless
|
96
|
+
dynamic_args += @xfvb_args if xfvb
|
97
|
+
dynamic_args << "--proxy-server=#{proxy[:host]}:#{proxy[:port]}" if proxy && proxy[:host] && proxy[:port]
|
98
|
+
dynamic_args << "--disable-features=#{disable_features.join(",")}" unless disable_features.empty?
|
99
|
+
|
100
|
+
@args + dynamic_args
|
101
|
+
end
|
102
|
+
|
103
|
+
def options
|
104
|
+
{
|
105
|
+
chrome_path: chrome_path,
|
106
|
+
user_data_dir: @user_data_dir,
|
107
|
+
headless: @headless,
|
108
|
+
xfvb: @xfvb,
|
109
|
+
native_control: @native_control,
|
110
|
+
args: @args,
|
111
|
+
headless_args: @headless_args,
|
112
|
+
xfvb_args: @xfvb_args,
|
113
|
+
exclude_switches: @exclude_switches,
|
114
|
+
proxy: @proxy,
|
115
|
+
disable_features: @disable_features
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|