rubium 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 124da1f21fad244bbdeb8adee803043896502e4c58964753bbba6ea8083df750
4
+ data.tar.gz: 13cda3fb2bd4f121700d08f03a8fbc3b15e38b785dca30074afa13121f4592b0
5
+ SHA512:
6
+ metadata.gz: e724053ddf9d97bbaf77db2c3647e1988c5bbace1a56b2f55f72afde1758ace0aaabd1f9d1771647361f8a863a6c33c35a8b616a7b8c4bd4f61278a750b33af0
7
+ data.tar.gz: 8decfacf86c9751c6ae39e61cae9b8fa1b29a3cb769f8ac3e11e15122364173bf0d20e649e058fe150023e1a7ea38ac13fd919d14abddef6fbfa961d64ca6987
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in rubium.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Victor Afanasev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,81 @@
1
+ # Rubium
2
+
3
+ Rubium is a handy wrapper around [chrome_remote](https://github.com/cavalle/chrome_remote) gem. It adds browsers instances handling, and some Capybara-like methods. It is very lightweight (200 lines of code in the main `Rubium::Browser` class for now) and doens't use Selenium or Capybara.
4
+
5
+ You can use Rubium as a lightweight alternative to Selenium/Capybara/Watir if you need to perform some operations (like web scraping) using Headless Chromium and Ruby. Of course, the API currently doesn't has a lot of methods to automate browser, but it has the most frequently used and basic ones.
6
+
7
+ ```ruby
8
+ require 'rubium'
9
+
10
+ browser = Rubium::Browser.new
11
+ browser.visit("https://github.com/vifreefly/rubium")
12
+
13
+ # Get current page response as string:
14
+ browser.body
15
+
16
+ # Get current page response as Nokogiri object:
17
+ browser.current_response
18
+
19
+ # Click to the some element (css selector):
20
+ browser.click("some selector")
21
+
22
+ # Get current cookies:
23
+ browser.cookies
24
+
25
+ # Fill in some field:
26
+ browser.fill_in("some field selector", "Some text")
27
+
28
+ # Tells if current response has provided css selector or not. You can
29
+ # provide optional `wait:` argument (in seconds) to set the max wait time for the selector:
30
+ browser.has_css?("some selector", wait: 1)
31
+
32
+ # Tells if current response has provided text or not. You can
33
+ # provide optional `wait:` argument (in seconds) to set the max wait time for the text:
34
+ browser.has_text?("some text")
35
+
36
+ # Evaluate some JS code on a new tab:
37
+ browser.evaluate_on_new_document(File.read "browser_inject.js")
38
+
39
+ # Evaluate JS code expression:
40
+ browser.execute_script("JS code string")
41
+
42
+ # Access chrome_remote client directly:
43
+ browser.client
44
+
45
+ # Close browser:
46
+ browser.close
47
+
48
+ # Restart browser:
49
+ browser.restart!
50
+ ```
51
+
52
+ There are some options which you can provide while creating browser instance:
53
+
54
+ ```ruby
55
+ browser = Rubium::Browser.new(
56
+ debugging_port: 9222, # custom debugging port
57
+ headless: false, # Run browser in normal (not headless) mode
58
+ user_agent: "Some user agent", # Custom user-agent
59
+ proxy_server: "http://1.1.1.1:8080", # Set proxy
60
+ )
61
+ ```
62
+
63
+ You can provide custom Chrome binary path this way:
64
+
65
+ ```ruby
66
+ Rubium.configure do |config|
67
+ config.chrome_path = "/path/to/chrome/binary"
68
+ end
69
+ ```
70
+
71
+
72
+ ## Installation
73
+ Rubium tested with `2.3.0` Ruby version and up.
74
+
75
+ Rubium is in the alpha stage (and therefore will have breaking updates in the future), so it's recommended to hard-code latest gem version in your Gemfile, like: `gem 'rubium', '0.1.0'`.
76
+
77
+ ## Contribution
78
+ Sure, feel free to fork and add new functionality.
79
+
80
+ ## License
81
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "rubium"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,42 @@
1
+ require 'ostruct'
2
+ require 'rubium/version'
3
+ require 'rubium/browser'
4
+
5
+ module Rubium
6
+ DEFAULT_PUPPETEER_ARGS = %w(
7
+ --disable-background-networking
8
+ --disable-background-timer-throttling
9
+ --disable-backgrounding-occluded-windows
10
+ --disable-breakpad
11
+ --disable-client-side-phishing-detection
12
+ --disable-default-apps
13
+ --disable-dev-shm-usage
14
+ --disable-extensions
15
+ --disable-features=site-per-process
16
+ --disable-hang-monitor
17
+ --disable-ipc-flooding-protection
18
+ --disable-popup-blocking
19
+ --disable-prompt-on-repost
20
+ --disable-renderer-backgrounding
21
+ --disable-sync
22
+ --disable-translate
23
+ --metrics-recording-only
24
+ --no-first-run
25
+ --safebrowsing-disable-auto-update
26
+ --enable-automation
27
+ --password-store=basic
28
+ --use-mock-keychain
29
+ --hide-scrollbars
30
+ --mute-audio
31
+ --no-sandbox
32
+ --disable-infobars
33
+ ).freeze
34
+
35
+ def self.configuration
36
+ @configuration ||= OpenStruct.new
37
+ end
38
+
39
+ def self.configure
40
+ yield(configuration)
41
+ end
42
+ end
@@ -0,0 +1,206 @@
1
+ require 'chrome_remote'
2
+ require 'nokogiri'
3
+ require 'random-port'
4
+ require 'cliver'
5
+ require 'timeout'
6
+ require 'securerandom'
7
+
8
+ at_exit do
9
+ Rubium::Browser.running_pids.each { |pid| Process.kill("HUP", pid) }
10
+ end
11
+
12
+ module Rubium
13
+ class Browser
14
+ class ConfigurationError < StandardError; end
15
+
16
+ MAX_CONNECT_WAIT_TIME = 2
17
+
18
+ class << self
19
+ def ports_pool
20
+ @pool ||= RandomPort::Pool.new
21
+ end
22
+
23
+ def running_pids
24
+ @running_pids ||= []
25
+ end
26
+ end
27
+
28
+ attr_reader :client, :devtools_url, :pid, :port, :options
29
+
30
+ def initialize(options = {})
31
+ @options = options
32
+ create_browser
33
+ end
34
+
35
+ def restart!
36
+ close
37
+ create_browser
38
+ end
39
+
40
+ def close
41
+ unless closed?
42
+ Process.kill("HUP", @pid)
43
+ self.class.running_pids.delete(@pid)
44
+ self.class.ports_pool.release(@port)
45
+
46
+ FileUtils.rm_rf(@data_dir) if Dir.exist?(@data_dir)
47
+ @closed = true
48
+ end
49
+ end
50
+
51
+ alias_method :destroy_driver!, :close
52
+
53
+ def closed?
54
+ @closed
55
+ end
56
+
57
+ def goto(url, wait: 30)
58
+ response = @client.send_cmd "Page.navigate", url: url
59
+
60
+ if wait
61
+ Timeout.timeout(wait) { @client.wait_for "Page.loadEventFired" }
62
+ else
63
+ response
64
+ end
65
+ end
66
+
67
+ alias_method :visit, :goto
68
+
69
+ def body
70
+ response = @client.send_cmd "Runtime.evaluate", expression: 'document.documentElement.outerHTML'
71
+ response.dig("result", "value")
72
+ end
73
+
74
+ def current_response
75
+ Nokogiri::HTML(body)
76
+ end
77
+
78
+ def has_xpath?(path, wait: 0)
79
+ timer = 0
80
+ until current_response.at_xpath(path)
81
+ return false if timer >= wait
82
+ timer += 0.2 and sleep 0.2
83
+ end
84
+
85
+ true
86
+ end
87
+
88
+ def has_css?(selector, wait: 0)
89
+ timer = 0
90
+ until current_response.at_css(selector)
91
+ return false if timer >= wait
92
+ timer += 0.2 and sleep 0.2
93
+ end
94
+
95
+ true
96
+ end
97
+
98
+ def has_text?(text, wait: 0)
99
+ timer = 0
100
+ until body&.include?(text)
101
+ return false if timer >= wait
102
+ timer += 0.2 and sleep 0.2
103
+ end
104
+
105
+ true
106
+ end
107
+
108
+ def click(selector)
109
+ @client.send_cmd "Runtime.evaluate", expression: <<~JS
110
+ document.querySelector("#{selector}").click();
111
+ JS
112
+ end
113
+
114
+ # https://github.com/cyrus-and/chrome-remote-interface/issues/226#issuecomment-320247756
115
+ # https://stackoverflow.com/a/18937620
116
+ def send_key_on(selector, key)
117
+ @client.send_cmd "Runtime.evaluate", expression: <<~JS
118
+ document.querySelector("#{selector}").dispatchEvent(
119
+ new KeyboardEvent("keydown", {
120
+ bubbles: true, cancelable: true, keyCode: #{key}
121
+ })
122
+ );
123
+ JS
124
+ end
125
+
126
+ # https://github.com/GoogleChrome/puppeteer/blob/master/lib/Page.js#L784
127
+ # https://stackoverflow.com/questions/46113267/how-to-use-evaluateonnewdocument-and-exposefunction
128
+ # https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-addScriptToEvaluateOnNewDocument
129
+ def evaluate_on_new_document(script)
130
+ @client.send_cmd "Page.addScriptToEvaluateOnNewDocument", source: script
131
+ end
132
+
133
+ def cookies
134
+ response = @client.send_cmd "Network.getCookies"
135
+ response["cookies"]
136
+ end
137
+
138
+ def fill_in(selector, text)
139
+ execute_script <<~HEREDOC
140
+ document.querySelector("#{selector}").value = "#{text}"
141
+ HEREDOC
142
+ end
143
+
144
+ def execute_script(script)
145
+ @client.send_cmd "Runtime.evaluate", expression: script
146
+ end
147
+
148
+ private
149
+
150
+ def create_browser
151
+ @port = options[:debugging_port] || self.class.ports_pool.acquire
152
+ @data_dir = "/tmp/rubium_profile_#{SecureRandom.hex}"
153
+
154
+ chrome_path = Rubium.configuration.chrome_path ||
155
+ Cliver.detect("chromium-browser") ||
156
+ Cliver.detect("google-chrome")
157
+ raise ConfigurationError, "Can't find chrome executable" unless chrome_path
158
+
159
+ command = %W(
160
+ #{chrome_path} about:blank
161
+ --remote-debugging-port=#{@port}
162
+ --user-data-dir=#{@data_dir}
163
+ ) + DEFAULT_PUPPETEER_ARGS
164
+
165
+ command << "--headless" if ENV["HEADLESS"] != "false" && options[:headless] != false
166
+ command << "--window-size=#{options[:window_size].join(',')}" if options[:window_size]
167
+
168
+ if options[:user_agent]
169
+ user_agent = options[:user_agent].respond_to?(:call) ? options[:user_agent].call : options[:user_agent]
170
+ command << "--user-agent=#{user_agent}"
171
+ end
172
+
173
+ if options[:proxy_server]
174
+ proxy_server = options[:proxy_server].respond_to?(:call) ? options[:proxy_server].call : options[:proxy_server]
175
+ proxy_server = convert_proxy(proxy_server) unless proxy_server.include?("://")
176
+ command << "--proxy-server=#{proxy_server}"
177
+ end
178
+
179
+ @pid = spawn(*command, [:out, :err] => "/dev/null")
180
+ self.class.running_pids << @pid
181
+ @closed = false
182
+
183
+ counter = 0
184
+ begin
185
+ counter += 0.2 and sleep 0.2
186
+ @client = ChromeRemote.client(port: @port)
187
+ rescue Errno::ECONNREFUSED => e
188
+ counter < MAX_CONNECT_WAIT_TIME ? retry : raise(e)
189
+ end
190
+
191
+ @devtools_url = "http://localhost:#{@port}/"
192
+
193
+ # https://github.com/GoogleChrome/puppeteer/blob/master/lib/Page.js
194
+ @client.send_cmd "Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: false
195
+ @client.send_cmd "Network.enable"
196
+ @client.send_cmd "Page.enable"
197
+
198
+ evaluate_on_new_document(options[:extension_code]) if options[:extension_code]
199
+ end
200
+
201
+ def convert_proxy(proxy_string)
202
+ ip, port, type, user, password = proxy_string.split(":")
203
+ "#{type}://#{ip}:#{port}"
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,3 @@
1
+ module Rubium
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,32 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "rubium/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "rubium"
8
+ spec.version = Rubium::VERSION
9
+ spec.authors = ["Victor Afanasev"]
10
+ spec.email = ["vicfreefly@gmail.com"]
11
+
12
+ spec.summary = "Headless Chromium Ruby API based on ChromeRemote gem"
13
+ spec.description = spec.summary
14
+ spec.homepage = "https://github.com/vifreefly/rubium"
15
+ spec.license = "MIT"
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "chrome_remote", "~> 0.2"
25
+ spec.add_dependency "cliver", "~> 0.3"
26
+ spec.add_dependency "random-port"
27
+ spec.add_dependency "nokogiri"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.16"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "minitest", "~> 5.0"
32
+ end
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubium
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Victor Afanasev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-12-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: chrome_remote
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: cliver
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: random-port
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rake
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '10.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '10.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
111
+ description: Headless Chromium Ruby API based on ChromeRemote gem
112
+ email:
113
+ - vicfreefly@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - bin/console
124
+ - bin/setup
125
+ - lib/rubium.rb
126
+ - lib/rubium/browser.rb
127
+ - lib/rubium/version.rb
128
+ - rubium.gemspec
129
+ homepage: https://github.com/vifreefly/rubium
130
+ licenses:
131
+ - MIT
132
+ metadata: {}
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 2.7.6
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Headless Chromium Ruby API based on ChromeRemote gem
153
+ test_files: []