puppeteer-ruby 0.0.2
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.
- 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
|