rubium 0.1.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.
@@ -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: []