puppeteer-ruby 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/README.md +41 -0
- data/Rakefile +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/example.rb +7 -0
- data/lib/puppeteer.rb +192 -0
- data/lib/puppeteer/async_await_behavior.rb +34 -0
- data/lib/puppeteer/browser.rb +240 -0
- data/lib/puppeteer/browser_context.rb +90 -0
- data/lib/puppeteer/browser_fetcher.rb +6 -0
- data/lib/puppeteer/browser_runner.rb +142 -0
- data/lib/puppeteer/cdp_session.rb +78 -0
- data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
- data/lib/puppeteer/connection.rb +254 -0
- data/lib/puppeteer/console_message.rb +24 -0
- data/lib/puppeteer/debug_print.rb +20 -0
- data/lib/puppeteer/device.rb +12 -0
- data/lib/puppeteer/devices.rb +885 -0
- data/lib/puppeteer/dom_world.rb +447 -0
- data/lib/puppeteer/element_handle.rb +433 -0
- data/lib/puppeteer/emulation_manager.rb +46 -0
- data/lib/puppeteer/errors.rb +4 -0
- data/lib/puppeteer/event_callbackable.rb +88 -0
- data/lib/puppeteer/execution_context.rb +230 -0
- data/lib/puppeteer/frame.rb +278 -0
- data/lib/puppeteer/frame_manager.rb +380 -0
- data/lib/puppeteer/if_present.rb +18 -0
- data/lib/puppeteer/js_handle.rb +142 -0
- data/lib/puppeteer/keyboard.rb +183 -0
- data/lib/puppeteer/keyboard/key_description.rb +19 -0
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
- data/lib/puppeteer/launcher.rb +26 -0
- data/lib/puppeteer/launcher/base.rb +48 -0
- data/lib/puppeteer/launcher/browser_options.rb +41 -0
- data/lib/puppeteer/launcher/chrome.rb +165 -0
- data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
- data/lib/puppeteer/launcher/launch_options.rb +68 -0
- data/lib/puppeteer/lifecycle_watcher.rb +168 -0
- data/lib/puppeteer/mouse.rb +120 -0
- data/lib/puppeteer/network_manager.rb +122 -0
- data/lib/puppeteer/page.rb +1001 -0
- data/lib/puppeteer/page/screenshot_options.rb +78 -0
- data/lib/puppeteer/remote_object.rb +124 -0
- data/lib/puppeteer/target.rb +150 -0
- data/lib/puppeteer/timeout_settings.rb +15 -0
- data/lib/puppeteer/touch_screen.rb +43 -0
- data/lib/puppeteer/version.rb +3 -0
- data/lib/puppeteer/viewport.rb +36 -0
- data/lib/puppeteer/wait_task.rb +6 -0
- data/lib/puppeteer/web_socket.rb +117 -0
- data/lib/puppeteer/web_socket_transport.rb +49 -0
- data/puppeteer-ruby.gemspec +29 -0
- metadata +213 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'mime/types'
|
2
|
+
|
3
|
+
class Puppeteer::Page
|
4
|
+
# /**
|
5
|
+
# * @typedef {Object} ScreenshotOptions
|
6
|
+
# * @property {string=} type
|
7
|
+
# * @property {string=} path
|
8
|
+
# * @property {boolean=} fullPage
|
9
|
+
# * @property {{x: number, y: number, width: number, height: number}=} clip
|
10
|
+
# * @property {number=} quality
|
11
|
+
# * @property {boolean=} omitBackground
|
12
|
+
# * @property {string=} encoding
|
13
|
+
# */
|
14
|
+
class ScreenshotOptions
|
15
|
+
# @params options [Hash]
|
16
|
+
def initialize(options)
|
17
|
+
if options[:type]
|
18
|
+
unless [:png, :jpeg].include?(options[:type].to_sym)
|
19
|
+
raise ArgumentError.new("Unknown options.type value: #{options[:type]}")
|
20
|
+
end
|
21
|
+
@type = options[:type]
|
22
|
+
elsif options[:path]
|
23
|
+
mime_types = MIME::Types.type_for(options[:path])
|
24
|
+
if mime_types.include?('image/png')
|
25
|
+
@type = 'png'
|
26
|
+
elsif mime_types.include?('image/jpeg')
|
27
|
+
@type = 'jpeg'
|
28
|
+
else
|
29
|
+
raise ArgumentError.new("Unsupported screenshot mime type resolved: #{mime_types}, path: #{options[:path]}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
@type ||= 'png'
|
33
|
+
|
34
|
+
if options[:quality]
|
35
|
+
unless @type == 'png'
|
36
|
+
raise ArgumentError.new("options.quality is unsupported for the #{@type} screenshots")
|
37
|
+
end
|
38
|
+
unless options[:quality].is_a?(Numeric)
|
39
|
+
raise ArgumentError.new("Expected options.quality to be a number but found #{options[:quality].class}")
|
40
|
+
end
|
41
|
+
quality = options[:quality].to_i
|
42
|
+
unless (0..100).include?(qualizy)
|
43
|
+
raise ArgumentError.new("Expected options.quality to be between 0 and 100 (inclusive), got #{quality}")
|
44
|
+
end
|
45
|
+
@quality = quality
|
46
|
+
end
|
47
|
+
|
48
|
+
if options[:clip] && options[:full_page]
|
49
|
+
raise ArgumentError.new('options.clip and options.fullPage are exclusive')
|
50
|
+
end
|
51
|
+
|
52
|
+
# if (options.clip) {
|
53
|
+
# assert(typeof options.clip.x === 'number', 'Expected options.clip.x to be a number but found ' + (typeof options.clip.x));
|
54
|
+
# assert(typeof options.clip.y === 'number', 'Expected options.clip.y to be a number but found ' + (typeof options.clip.y));
|
55
|
+
# assert(typeof options.clip.width === 'number', 'Expected options.clip.width to be a number but found ' + (typeof options.clip.width));
|
56
|
+
# assert(typeof options.clip.height === 'number', 'Expected options.clip.height to be a number but found ' + (typeof options.clip.height));
|
57
|
+
# assert(options.clip.width !== 0, 'Expected options.clip.width not to be 0.');
|
58
|
+
# assert(options.clip.height !== 0, 'Expected options.clip.height not to be 0.');
|
59
|
+
# }
|
60
|
+
|
61
|
+
@path = options[:path]
|
62
|
+
@full_page = options[:full_page]
|
63
|
+
@clip = options[:clip]
|
64
|
+
@omit_background = options[:omit_background]
|
65
|
+
@encoding = options[:encoding]
|
66
|
+
end
|
67
|
+
|
68
|
+
attr_reader :type, :quality, :path, :clip, :encoding
|
69
|
+
|
70
|
+
def full_page?
|
71
|
+
@full_page
|
72
|
+
end
|
73
|
+
|
74
|
+
def omit_background?
|
75
|
+
@omit_background
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# providing #valueFromRemoteObject, #releaseObject
|
2
|
+
class Puppeteer::RemoteObject
|
3
|
+
include Puppeteer::DebugPrint
|
4
|
+
using Puppeteer::AsyncAwaitBehavior
|
5
|
+
|
6
|
+
# @param payload [Hash]
|
7
|
+
def initialize(payload)
|
8
|
+
@object_id = payload["objectId"]
|
9
|
+
@sub_type = payload["subtype"]
|
10
|
+
@unserializable_value = payload["unserializableValue"]
|
11
|
+
@value = payload["value"]
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :sub_type
|
15
|
+
|
16
|
+
# @return [Future<Puppeteer::RemoteObject|nil>]
|
17
|
+
def evaluate_self(client)
|
18
|
+
# ported logic from JSHandle#json_value.
|
19
|
+
|
20
|
+
# original logic:
|
21
|
+
# if (this._remoteObject.objectId) {
|
22
|
+
# const response = await this._client.send('Runtime.callFunctionOn', {
|
23
|
+
# functionDeclaration: 'function() { return this; }',
|
24
|
+
# objectId: this._remoteObject.objectId,
|
25
|
+
# returnByValue: true,
|
26
|
+
# awaitPromise: true,
|
27
|
+
# });
|
28
|
+
# return helper.valueFromRemoteObject(response.result);
|
29
|
+
# }
|
30
|
+
|
31
|
+
if @object_id
|
32
|
+
params = {
|
33
|
+
'functionDeclaration': 'function() { return this; }',
|
34
|
+
'objectId': @object_id,
|
35
|
+
'returnByValue': true,
|
36
|
+
'awaitPromise': true,
|
37
|
+
}
|
38
|
+
response = client.send_message("Runtime.callFunctionOn", params)
|
39
|
+
Puppeteer::RemoteObject.new(response["result"])
|
40
|
+
else
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# used in JSHandle#properties
|
46
|
+
def properties(client)
|
47
|
+
# original logic:
|
48
|
+
# const response = await this._client.send('Runtime.getProperties', {
|
49
|
+
# objectId: this._remoteObject.objectId,
|
50
|
+
# ownProperties: true
|
51
|
+
# });
|
52
|
+
client.send_message('Runtime.getProperties', objectId: @object_id, ownProperties: true)
|
53
|
+
end
|
54
|
+
|
55
|
+
# used in ElementHandle#content_frame
|
56
|
+
def node_info(client)
|
57
|
+
client.send_message("DOM.describeNode", objectId: @object_id)
|
58
|
+
end
|
59
|
+
|
60
|
+
# helper#valueFromRemoteObject
|
61
|
+
def value
|
62
|
+
if @unserializable_value
|
63
|
+
# if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined')
|
64
|
+
# return BigInt(remoteObject.unserializableValue.replace('n', ''));
|
65
|
+
# switch (remoteObject.unserializableValue) {
|
66
|
+
# case '-0':
|
67
|
+
# return -0;
|
68
|
+
# case 'NaN':
|
69
|
+
# return NaN;
|
70
|
+
# case 'Infinity':
|
71
|
+
# return Infinity;
|
72
|
+
# case '-Infinity':
|
73
|
+
# return -Infinity;
|
74
|
+
# default:
|
75
|
+
# throw new Error('Unsupported unserializable value: ' + remoteObject.unserializableValue);
|
76
|
+
# }
|
77
|
+
raise NotImplementedError.new("unserializable_value is not implemented yet")
|
78
|
+
else
|
79
|
+
@value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param client [Puppeteer::CDPSession]
|
84
|
+
def release(client)
|
85
|
+
return unless @object_id
|
86
|
+
|
87
|
+
begin
|
88
|
+
client.send_message('Runtime.releaseObject',
|
89
|
+
objectId: @object_id,
|
90
|
+
)
|
91
|
+
rescue => err
|
92
|
+
# Exceptions might happen in case of a page been navigated or closed.
|
93
|
+
# Swallow these since they are harmless and we don't leak anything in this case.
|
94
|
+
debug_puts(err)
|
95
|
+
end
|
96
|
+
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# @param client [Puppeteer::CDPSession]
|
101
|
+
async def async_release(client)
|
102
|
+
release(client)
|
103
|
+
end
|
104
|
+
|
105
|
+
def converted_arg
|
106
|
+
# ported logic from ExecutionContext#convertArgument
|
107
|
+
# https://github.com/puppeteer/puppeteer/blob/master/lib/ExecutionContext.js
|
108
|
+
#
|
109
|
+
# Original logic:
|
110
|
+
# if (objectHandle._remoteObject.unserializableValue)
|
111
|
+
# return { unserializableValue: objectHandle._remoteObject.unserializableValue };
|
112
|
+
# if (!objectHandle._remoteObject.objectId)
|
113
|
+
# return { value: objectHandle._remoteObject.value };
|
114
|
+
# return { objectId: objectHandle._remoteObject.objectId };
|
115
|
+
|
116
|
+
if @unserializable_value
|
117
|
+
{ unserializableValue: @unserializable_value }
|
118
|
+
elsif @object_id
|
119
|
+
{ objectId: @object_id }
|
120
|
+
else
|
121
|
+
{ value: value }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# https://github.com/puppeteer/puppeteer/blob/master/lib/Target.js
|
2
|
+
class Puppeteer::Target
|
3
|
+
class TargetInfo
|
4
|
+
def initialize(options)
|
5
|
+
@target_id = options['targetId']
|
6
|
+
@type = options['type']
|
7
|
+
@title = options['title']
|
8
|
+
@url = options['url']
|
9
|
+
@attached = options['attached']
|
10
|
+
@browser_context_id = options['browserContextId']
|
11
|
+
@opener_id = options['openerId']
|
12
|
+
end
|
13
|
+
attr_reader :target_id, :type, :title, :url, :attached, :browser_context_id, :opener_id
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param {!Protocol.Target.TargetInfo} targetInfo
|
17
|
+
# @param {!Puppeteer.BrowserContext} browserContext
|
18
|
+
# @param {!function():!Promise<!Puppeteer.CDPSession>} sessionFactory
|
19
|
+
# @param {boolean} ignoreHTTPSErrors
|
20
|
+
# @param {?Puppeteer.Viewport} defaultViewport
|
21
|
+
# @param {!Puppeteer.TaskQueue} screenshotTaskQueue
|
22
|
+
def initialize(target_info:, browser_context:, session_factory:, ignore_https_errors:, default_viewport:, screenshot_task_queue:)
|
23
|
+
@target_info = target_info
|
24
|
+
@browser_context = browser_context
|
25
|
+
@target_id = target_info.target_id
|
26
|
+
@session_factory = session_factory
|
27
|
+
@ignore_https_errors = ignore_https_errors
|
28
|
+
@default_viewport = default_viewport
|
29
|
+
@screenshot_task_queue = screenshot_task_queue
|
30
|
+
|
31
|
+
|
32
|
+
# /** @type {?Promise<!Puppeteer.Page>} */
|
33
|
+
# this._pagePromise = null;
|
34
|
+
# /** @type {?Promise<!Worker>} */
|
35
|
+
# this._workerPromise = null;
|
36
|
+
@initialized_promise = resolvable_future
|
37
|
+
# this._isClosedPromise = new Promise(fulfill => this._closedCallback = fulfill);
|
38
|
+
|
39
|
+
@is_initialized = @target_info.type != 'page' || !@target_info.url.empty?
|
40
|
+
|
41
|
+
if @is_initialized
|
42
|
+
handle_initialized(true)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :target_id, :initialized_promise
|
47
|
+
|
48
|
+
class InitializeFailure < StandardError ; end
|
49
|
+
|
50
|
+
def handle_initialized(success)
|
51
|
+
unless success
|
52
|
+
@initialized_promise.reject(InitializeFailure.new("Failed to create target for page"))
|
53
|
+
end
|
54
|
+
@on_initialize_succeeded&.call
|
55
|
+
@initialized_promise.fulfill(true)
|
56
|
+
opener_page = opener&.page
|
57
|
+
if opener_page.nil? || type != 'page'
|
58
|
+
return
|
59
|
+
end
|
60
|
+
# if (!openerPage.listenerCount(Events.Page.Popup))
|
61
|
+
# return true;
|
62
|
+
popup_page = page
|
63
|
+
opener_page.emit_event('Events.Page.Popup', popup_page)
|
64
|
+
end
|
65
|
+
|
66
|
+
def on_initialize_succeeded(&block)
|
67
|
+
@on_initialize_succeeded = block
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_closed
|
71
|
+
@closed = true
|
72
|
+
@on_close&.call
|
73
|
+
end
|
74
|
+
|
75
|
+
def on_close(&block)
|
76
|
+
@on_close = block
|
77
|
+
end
|
78
|
+
|
79
|
+
def initialized?
|
80
|
+
@is_initialized
|
81
|
+
end
|
82
|
+
|
83
|
+
def create_cdp_session
|
84
|
+
@session_factory.call
|
85
|
+
end
|
86
|
+
|
87
|
+
def page
|
88
|
+
if ['page', 'background_page'].include?(@target_info.type) && @page.nil?
|
89
|
+
client = @session_factory.call
|
90
|
+
@page = Puppeteer::Page.create(client, self, @ignore_https_errors, @default_viewport, @screenshot_task_queue)
|
91
|
+
end
|
92
|
+
@page
|
93
|
+
end
|
94
|
+
|
95
|
+
# /**
|
96
|
+
# * @return {!Promise<?Worker>}
|
97
|
+
# */
|
98
|
+
# async worker() {
|
99
|
+
# if (this._targetInfo.type !== 'service_worker' && this._targetInfo.type !== 'shared_worker')
|
100
|
+
# return null;
|
101
|
+
# if (!this._workerPromise) {
|
102
|
+
# // TODO(einbinder): Make workers send their console logs.
|
103
|
+
# this._workerPromise = this._sessionFactory()
|
104
|
+
# .then(client => new Worker(client, this._targetInfo.url, () => {} /* consoleAPICalled */, () => {} /* exceptionThrown */));
|
105
|
+
# }
|
106
|
+
# return this._workerPromise;
|
107
|
+
# }
|
108
|
+
|
109
|
+
# @return {string}
|
110
|
+
def url
|
111
|
+
@target_info.url
|
112
|
+
end
|
113
|
+
|
114
|
+
# @return {"page"|"background_page"|"service_worker"|"shared_worker"|"other"|"browser"}
|
115
|
+
def type
|
116
|
+
type = @target_info.type
|
117
|
+
if ['page', 'background_page', 'service_worker', 'shared_worker', 'browser'].include?(type)
|
118
|
+
type
|
119
|
+
else
|
120
|
+
'other'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# @return {!Puppeteer.Browser}
|
125
|
+
def browser
|
126
|
+
@browser_context.browser
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return {!Puppeteer.BrowserContext}
|
130
|
+
def browser_context
|
131
|
+
@browser_context
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return {?Puppeteer.Target}
|
135
|
+
def opener
|
136
|
+
opener_id = @target_info.opener_id
|
137
|
+
return nil if opener_id.nil?
|
138
|
+
browser.targets[opener_id]
|
139
|
+
end
|
140
|
+
|
141
|
+
# @param {!Protocol.Target.TargetInfo} targetInfo
|
142
|
+
def handle_target_info_changed(target_info)
|
143
|
+
@target_info = target_info
|
144
|
+
|
145
|
+
if !@is_initialized && (@target_info.type != 'page' || !target_info.url.empty?)
|
146
|
+
@is_initialized = true
|
147
|
+
handle_initialized(true)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Puppeteer::TimeoutSettings
|
2
|
+
DEFAULT_TIMEOUT = 30000
|
3
|
+
|
4
|
+
attr_writer :default_timeout, :default_navigation_timeout
|
5
|
+
|
6
|
+
# @return {number}
|
7
|
+
def navigation_timeout
|
8
|
+
@default_navigation_timeout || @default_timeout || DEFAULT_TIMEOUT
|
9
|
+
end
|
10
|
+
|
11
|
+
# @return {number}
|
12
|
+
def timeout
|
13
|
+
@default_timeout || DEFAULT_TIMEOUT
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Puppeteer::TouchScreen
|
2
|
+
using Puppeteer::AsyncAwaitBehavior
|
3
|
+
|
4
|
+
# @param {Puppeteer.CDPSession} client
|
5
|
+
# @param keyboard [Puppeteer::Keyboard]
|
6
|
+
def initialize(client, keyboard)
|
7
|
+
@client = client
|
8
|
+
@keyboard = keyboard
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param x [number]
|
12
|
+
# @param y [number]
|
13
|
+
def tap(x, y)
|
14
|
+
# Touches appear to be lost during the first frame after navigation.
|
15
|
+
# This waits a frame before sending the tap.
|
16
|
+
# @see https://crbug.com/613219
|
17
|
+
@client.send_message('Runtime.evaluate',
|
18
|
+
expression: 'new Promise(x => requestAnimationFrame(() => requestAnimationFrame(x)))',
|
19
|
+
awaitPromise: true,
|
20
|
+
)
|
21
|
+
|
22
|
+
touch_points = [
|
23
|
+
{ x: x.round, y: y.round },
|
24
|
+
]
|
25
|
+
@client.send_message('Input.dispatchTouchEvent',
|
26
|
+
type: 'touchStart',
|
27
|
+
touchPoints: touch_points,
|
28
|
+
modifiers: @keyboard.modifiers,
|
29
|
+
)
|
30
|
+
@client.send_message('Input.dispatchTouchEvent',
|
31
|
+
type: 'touchEnd',
|
32
|
+
touchPoints: [],
|
33
|
+
modifiers: @keyboard.modifiers,
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param x [number]
|
38
|
+
# @param y [number]
|
39
|
+
# @return [Future]
|
40
|
+
async def async_tap(x, y)
|
41
|
+
tap(x, y)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
class Puppeteer::Viewport
|
2
|
+
# @param width [int]
|
3
|
+
# @param height [int]
|
4
|
+
# @param device_scale_factor [double]
|
5
|
+
# @param is_mobile [boolean]
|
6
|
+
# @param has_touch [boolean]
|
7
|
+
# @param is_landscape [boolean]
|
8
|
+
def initialize(
|
9
|
+
width:,
|
10
|
+
height:,
|
11
|
+
device_scale_factor: 1.0,
|
12
|
+
is_mobile: false,
|
13
|
+
has_touch: false,
|
14
|
+
is_landscape: false)
|
15
|
+
@width = width
|
16
|
+
@height = height
|
17
|
+
@device_scale_factor = device_scale_factor
|
18
|
+
@is_mobile = is_mobile
|
19
|
+
@has_touch = has_touch
|
20
|
+
@is_landscape = is_landscape
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :width, :height, :device_scale_factor
|
24
|
+
|
25
|
+
def mobile?
|
26
|
+
@is_mobile
|
27
|
+
end
|
28
|
+
|
29
|
+
def has_touch?
|
30
|
+
@has_touch
|
31
|
+
end
|
32
|
+
|
33
|
+
def landscape?
|
34
|
+
@is_landscape
|
35
|
+
end
|
36
|
+
end
|