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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +36 -0
  5. data/.travis.yml +7 -0
  6. data/Dockerfile +6 -0
  7. data/Gemfile +6 -0
  8. data/README.md +41 -0
  9. data/Rakefile +1 -0
  10. data/bin/console +11 -0
  11. data/bin/setup +8 -0
  12. data/docker-compose.yml +15 -0
  13. data/example.rb +7 -0
  14. data/lib/puppeteer.rb +192 -0
  15. data/lib/puppeteer/async_await_behavior.rb +34 -0
  16. data/lib/puppeteer/browser.rb +240 -0
  17. data/lib/puppeteer/browser_context.rb +90 -0
  18. data/lib/puppeteer/browser_fetcher.rb +6 -0
  19. data/lib/puppeteer/browser_runner.rb +142 -0
  20. data/lib/puppeteer/cdp_session.rb +78 -0
  21. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  22. data/lib/puppeteer/connection.rb +254 -0
  23. data/lib/puppeteer/console_message.rb +24 -0
  24. data/lib/puppeteer/debug_print.rb +20 -0
  25. data/lib/puppeteer/device.rb +12 -0
  26. data/lib/puppeteer/devices.rb +885 -0
  27. data/lib/puppeteer/dom_world.rb +447 -0
  28. data/lib/puppeteer/element_handle.rb +433 -0
  29. data/lib/puppeteer/emulation_manager.rb +46 -0
  30. data/lib/puppeteer/errors.rb +4 -0
  31. data/lib/puppeteer/event_callbackable.rb +88 -0
  32. data/lib/puppeteer/execution_context.rb +230 -0
  33. data/lib/puppeteer/frame.rb +278 -0
  34. data/lib/puppeteer/frame_manager.rb +380 -0
  35. data/lib/puppeteer/if_present.rb +18 -0
  36. data/lib/puppeteer/js_handle.rb +142 -0
  37. data/lib/puppeteer/keyboard.rb +183 -0
  38. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  39. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  40. data/lib/puppeteer/launcher.rb +26 -0
  41. data/lib/puppeteer/launcher/base.rb +48 -0
  42. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  43. data/lib/puppeteer/launcher/chrome.rb +165 -0
  44. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  45. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  46. data/lib/puppeteer/lifecycle_watcher.rb +168 -0
  47. data/lib/puppeteer/mouse.rb +120 -0
  48. data/lib/puppeteer/network_manager.rb +122 -0
  49. data/lib/puppeteer/page.rb +1001 -0
  50. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  51. data/lib/puppeteer/remote_object.rb +124 -0
  52. data/lib/puppeteer/target.rb +150 -0
  53. data/lib/puppeteer/timeout_settings.rb +15 -0
  54. data/lib/puppeteer/touch_screen.rb +43 -0
  55. data/lib/puppeteer/version.rb +3 -0
  56. data/lib/puppeteer/viewport.rb +36 -0
  57. data/lib/puppeteer/wait_task.rb +6 -0
  58. data/lib/puppeteer/web_socket.rb +117 -0
  59. data/lib/puppeteer/web_socket_transport.rb +49 -0
  60. data/puppeteer-ruby.gemspec +29 -0
  61. 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,3 @@
1
+ class Puppeteer
2
+ VERSION = "0.0.2"
3
+ 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