puppeteer-ruby 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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