puppeteer-ruby 0.0.6 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -2
  3. data/docs/Puppeteer.html +432 -104
  4. data/docs/Puppeteer/AsyncAwaitBehavior.html +1 -1
  5. data/docs/Puppeteer/Browser.html +261 -153
  6. data/docs/Puppeteer/BrowserContext.html +2 -2
  7. data/docs/Puppeteer/BrowserFetcher.html +1 -1
  8. data/docs/Puppeteer/BrowserRunner.html +1 -1
  9. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +1 -1
  10. data/docs/Puppeteer/CDPSession.html +2 -2
  11. data/docs/Puppeteer/CDPSession/Error.html +1 -1
  12. data/docs/Puppeteer/ConcurrentRubyUtils.html +14 -6
  13. data/docs/Puppeteer/Connection.html +66 -62
  14. data/docs/Puppeteer/Connection/MessageCallback.html +1 -1
  15. data/docs/Puppeteer/Connection/ProtocolError.html +1 -1
  16. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +5 -5
  17. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +12 -12
  18. data/docs/Puppeteer/ConsoleMessage.html +1 -1
  19. data/docs/Puppeteer/ConsoleMessage/Location.html +1 -1
  20. data/docs/Puppeteer/DOMWorld.html +130 -33
  21. data/docs/Puppeteer/DOMWorld/DetachedError.html +1 -1
  22. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +1 -1
  23. data/docs/Puppeteer/DebugPrint.html +1 -1
  24. data/docs/Puppeteer/Device.html +1 -1
  25. data/docs/Puppeteer/Devices.html +1 -1
  26. data/docs/Puppeteer/ElementHandle.html +960 -218
  27. data/docs/Puppeteer/ElementHandle/BoundingBox.html +507 -0
  28. data/docs/Puppeteer/ElementHandle/BoxModel.html +404 -0
  29. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +5 -5
  30. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +5 -5
  31. data/docs/Puppeteer/ElementHandle/Point.html +40 -29
  32. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +1 -1
  33. data/docs/Puppeteer/EmulationManager.html +1 -1
  34. data/docs/Puppeteer/EventCallbackable.html +83 -17
  35. data/docs/Puppeteer/EventCallbackable/EventListeners.html +1 -1
  36. data/docs/Puppeteer/ExecutionContext.html +114 -11
  37. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +1 -1
  38. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +1 -1
  39. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +1 -1
  40. data/docs/Puppeteer/FileChooser.html +455 -0
  41. data/docs/Puppeteer/Frame.html +425 -289
  42. data/docs/Puppeteer/FrameManager.html +23 -27
  43. data/docs/Puppeteer/FrameManager/NavigationError.html +1 -1
  44. data/docs/Puppeteer/IfPresent.html +1 -1
  45. data/docs/Puppeteer/JSHandle.html +1 -1
  46. data/docs/Puppeteer/Keyboard.html +1 -1
  47. data/docs/Puppeteer/Keyboard/KeyDefinition.html +1 -1
  48. data/docs/Puppeteer/Keyboard/KeyDescription.html +1 -1
  49. data/docs/Puppeteer/Launcher.html +1 -1
  50. data/docs/Puppeteer/Launcher/Base.html +1 -1
  51. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +1 -1
  52. data/docs/Puppeteer/Launcher/BrowserOptions.html +1 -1
  53. data/docs/Puppeteer/Launcher/Chrome.html +64 -23
  54. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +1 -1
  55. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +1 -1
  56. data/docs/Puppeteer/Launcher/LaunchOptions.html +1 -1
  57. data/docs/Puppeteer/LifecycleWatcher.html +1 -1
  58. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +1 -1
  59. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +1 -1
  60. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +1 -1
  61. data/docs/Puppeteer/Mouse.html +31 -41
  62. data/docs/Puppeteer/Mouse/Button.html +1 -1
  63. data/docs/Puppeteer/NetworkManager.html +2 -2
  64. data/docs/Puppeteer/NetworkManager/Credentials.html +1 -1
  65. data/docs/Puppeteer/Page.html +1049 -354
  66. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  67. data/docs/Puppeteer/Page/ScreenshotOptions.html +1 -1
  68. data/docs/Puppeteer/Page/ScriptTag.html +24 -24
  69. data/docs/Puppeteer/Page/StyleTag.html +19 -19
  70. data/docs/Puppeteer/Page/TargetCrashedError.html +1 -1
  71. data/docs/Puppeteer/RemoteObject.html +173 -37
  72. data/docs/Puppeteer/Target.html +150 -198
  73. data/docs/Puppeteer/Target/InitializeFailure.html +1 -1
  74. data/docs/Puppeteer/Target/TargetInfo.html +1 -1
  75. data/docs/Puppeteer/TimeoutError.html +2 -2
  76. data/docs/Puppeteer/TimeoutSettings.html +1 -1
  77. data/docs/Puppeteer/TouchScreen.html +1 -1
  78. data/docs/Puppeteer/Viewport.html +81 -1
  79. data/docs/Puppeteer/WaitTask.html +1 -1
  80. data/docs/Puppeteer/WaitTask/TerminatedError.html +1 -1
  81. data/docs/Puppeteer/WaitTask/TimeoutError.html +1 -1
  82. data/docs/Puppeteer/WebSocket.html +26 -26
  83. data/docs/Puppeteer/WebSocket/DriverImpl.html +1 -1
  84. data/docs/Puppeteer/WebSocket/TransportError.html +124 -0
  85. data/docs/Puppeteer/WebSocketTransport.html +9 -9
  86. data/docs/Puppeteer/WebSocktTransportError.html +1 -1
  87. data/docs/_index.html +40 -19
  88. data/docs/class_list.html +1 -1
  89. data/docs/file.README.html +7 -3
  90. data/docs/index.html +7 -3
  91. data/docs/method_list.html +785 -513
  92. data/docs/top-level-namespace.html +1 -1
  93. data/lib/puppeteer.rb +43 -13
  94. data/lib/puppeteer/browser.rb +26 -6
  95. data/lib/puppeteer/browser_runner.rb +1 -1
  96. data/lib/puppeteer/concurrent_ruby_utils.rb +6 -2
  97. data/lib/puppeteer/connection.rb +13 -1
  98. data/lib/puppeteer/dom_world.rb +16 -18
  99. data/lib/puppeteer/element_handle.rb +147 -168
  100. data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
  101. data/lib/puppeteer/element_handle/box_model.rb +19 -0
  102. data/lib/puppeteer/element_handle/point.rb +26 -0
  103. data/lib/puppeteer/errors.rb +1 -3
  104. data/lib/puppeteer/event_callbackable.rb +11 -0
  105. data/lib/puppeteer/execution_context.rb +13 -0
  106. data/lib/puppeteer/file_chooser.rb +29 -0
  107. data/lib/puppeteer/frame.rb +19 -1
  108. data/lib/puppeteer/frame_manager.rb +0 -2
  109. data/lib/puppeteer/launcher.rb +6 -6
  110. data/lib/puppeteer/launcher/chrome.rb +49 -3
  111. data/lib/puppeteer/mouse.rb +3 -8
  112. data/lib/puppeteer/page.rb +126 -54
  113. data/lib/puppeteer/remote_object.rb +15 -1
  114. data/lib/puppeteer/target.rb +25 -25
  115. data/lib/puppeteer/version.rb +1 -1
  116. data/lib/puppeteer/viewport.rb +18 -0
  117. data/lib/puppeteer/web_socket.rb +3 -1
  118. data/lib/puppeteer/web_socket_transport.rb +8 -8
  119. data/puppeteer-ruby.png +0 -0
  120. metadata +12 -4
  121. data/Dockerfile +0 -6
  122. data/docker-compose.yml +0 -15
@@ -116,7 +116,7 @@
116
116
  </div>
117
117
 
118
118
  <div id="footer">
119
- Generated on Mon May 25 23:22:48 2020 by
119
+ Generated on Tue Jun 23 10:24:58 2020 by
120
120
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
121
121
  0.9.24 (ruby-2.6.3).
122
122
  </div>
@@ -25,6 +25,7 @@ require 'puppeteer/devices'
25
25
  require 'puppeteer/dom_world'
26
26
  require 'puppeteer/emulation_manager'
27
27
  require 'puppeteer/execution_context'
28
+ require 'puppeteer/file_chooser'
28
29
  require 'puppeteer/frame'
29
30
  require 'puppeteer/frame_manager'
30
31
  require 'puppeteer/js_handle'
@@ -62,17 +63,33 @@ class Puppeteer
62
63
  end
63
64
  end
64
65
 
65
- # @param {string} projectRoot
66
- # @param {string} preferredRevision
67
- # @param {boolean} isPuppeteerCore
66
+ # @param project_root [String]
67
+ # @param prefereed_revision [String]
68
+ # @param is_puppeteer_core [String]
68
69
  def initialize(project_root:, preferred_revision:, is_puppeteer_core:)
69
70
  @project_root = project_root
70
71
  @preferred_revision = preferred_revision
71
72
  @is_puppeteer_core = is_puppeteer_core
72
73
  end
73
74
 
74
- # @param {!(Launcher.LaunchOptions & Launcher.ChromeArgOptions & Launcher.BrowserOptions & {product?: string, extraPrefsFirefox?: !object})=} options
75
- # @return {!Promise<!Puppeteer.Browser>}
75
+ # @param product [String]
76
+ # @param executable_path [String]
77
+ # @param ignore_default_args [Array<String>|nil]
78
+ # @param handle_SIGINT [Boolean]
79
+ # @param handle_SIGTERM [Boolean]
80
+ # @param handle_SIGHUP [Boolean]
81
+ # @param timeout [Integer]
82
+ # @param dumpio [Boolean]
83
+ # @param env [Hash]
84
+ # @param pipe [Boolean]
85
+ # @param args [Array<String>]
86
+ # @param user_data_dir [String]
87
+ # @param devtools [Boolean]
88
+ # @param headless [Boolean]
89
+ # @param ignore_https_errors [Boolean]
90
+ # @param default_viewport [Puppeteer::Viewport|nil]
91
+ # @param slow_mo [Integer]
92
+ # @return [Puppeteer::Browser]
76
93
  def launch(
77
94
  product: nil,
78
95
  executable_path: nil,
@@ -124,8 +141,13 @@ class Puppeteer
124
141
  end
125
142
  end
126
143
 
127
- # @param {!(Launcher.BrowserOptions & {browserWSEndpoint?: string, browserURL?: string, transport?: !Puppeteer.ConnectionTransport})} options
128
- # @return {!Promise<!Puppeteer.Browser>}
144
+ # @param browser_ws_endpoint [String]
145
+ # @param browser_url [String]
146
+ # @param transport [Puppeteer::WebSocketTransport]
147
+ # @param ignore_https_errors [Boolean]
148
+ # @param default_viewport [Puppeteer::Viewport|nil]
149
+ # @param slow_mo [Integer]
150
+ # @return [Puppeteer::Browser]
129
151
  def connect(
130
152
  browser_ws_endpoint: nil,
131
153
  browser_url: nil,
@@ -142,10 +164,15 @@ class Puppeteer
142
164
  default_viewport: default_viewport,
143
165
  slow_mo: slow_mo,
144
166
  }.compact
145
- launcher.connect(options)
167
+ browser = launcher.connect(options)
168
+ if block_given?
169
+ yield(browser)
170
+ else
171
+ browser
172
+ end
146
173
  end
147
174
 
148
- # @return {string}
175
+ # @return [String]
149
176
  def executable_path
150
177
  launcher.executable_path
151
178
  end
@@ -159,12 +186,12 @@ class Puppeteer
159
186
  )
160
187
  end
161
188
 
162
- # @return {string}
189
+ # @return [String]
163
190
  def product
164
191
  launcher.product
165
192
  end
166
193
 
167
- # @return {Puppeteer::Devices}
194
+ # @return [Puppeteer::Devices]
168
195
  def devices
169
196
  Puppeteer::Devices
170
197
  end
@@ -174,8 +201,11 @@ class Puppeteer
174
201
  # # ???
175
202
  # end
176
203
 
177
- # @param {!Launcher.ChromeArgOptions=} options
178
- # @return {!Array<string>}
204
+ # @param args [Array<String>]
205
+ # @param user_data_dir [String]
206
+ # @param devtools [Boolean]
207
+ # @param headless [Boolean]
208
+ # @return [Array<String>]
179
209
  def default_args(args: nil, user_data_dir: nil, devtools: nil, headless: nil)
180
210
  options = {
181
211
  args: args,
@@ -46,7 +46,7 @@ class Puppeteer::Browser
46
46
  @contexts[context_id] = Puppeteer::BrowserContext.new(@connection, self. context_id)
47
47
  end
48
48
  @targets = {}
49
- @connection.on_event 'Events.CDPSession.Disconnected' do
49
+ @connection.on_event 'Events.Connection.Disconnected' do
50
50
  emit_event 'Events.Browser.Disconnected'
51
51
  end
52
52
  @connection.on_event 'Target.targetCreated', &method(:handle_target_created)
@@ -54,6 +54,22 @@ class Puppeteer::Browser
54
54
  @connection.on_event 'Target.targetInfoChanged', &method(:handle_target_info_changed)
55
55
  end
56
56
 
57
+ EVENT_MAPPINGS = {
58
+ disconnected: 'Events.Browser.Disconnected',
59
+ targetcreated: 'Events.Browser.TargetCreated',
60
+ targetchanged: 'Events.Browser.TargetChanged',
61
+ targetdestroyed: 'Events.Browser.TargetDestroyed',
62
+ }
63
+
64
+ # @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
65
+ def on(event_name, &block)
66
+ unless EVENT_MAPPINGS.has_key?(event_name.to_sym)
67
+ raise ArgumentError.new("Unknown event name: #{event_name}. Known events are #{EVENT_MAPPINGS.keys.join(", ")}")
68
+ end
69
+
70
+ add_event_listener(EVENT_MAPPINGS[event_name.to_sym], &block)
71
+ end
72
+
57
73
  # @return [Puppeteer::BrowserRunner::BrowserProcess]
58
74
  def process
59
75
  @process
@@ -102,8 +118,7 @@ class Puppeteer::Browser
102
118
  )
103
119
  # assert(!this._targets.has(event.targetInfo.targetId), 'Target should not exist before targetCreated');
104
120
  @targets[target_info.target_id] = target
105
-
106
- target.on_initialize_succeeded do
121
+ if await target.initialized_promise
107
122
  emit_event 'Events.Browser.TargetCreated', target
108
123
  context.emit_event 'Events.BrowserContext.TargetCreated', target
109
124
  end
@@ -118,10 +133,10 @@ class Puppeteer::Browser
118
133
  def handle_target_destroyed(event)
119
134
  target_id = event['targetId']
120
135
  target = @targets[target_id]
121
- target.handle_initialized(false)
136
+ target.ignore_initialize_callback_promise
122
137
  @targets.delete(target_id)
123
- target.handle_closed
124
- target.on_initialize_succeeded do
138
+ target.closed_callback
139
+ if await target.initialized_promise
125
140
  emit_event 'Events.Browser.TargetDestroyed', target
126
141
  target.browser_context.emit_event 'Events.BrowserContext.TargetDestroyed', target
127
142
  end
@@ -192,6 +207,11 @@ class Puppeteer::Browser
192
207
  targets.first { |target| target.type == 'browser' }
193
208
  end
194
209
 
210
+ # used only in Target#opener
211
+ private def find_target_by_id(target_id)
212
+ @targets[target_id]
213
+ end
214
+
195
215
  # @param {function(!Target):boolean} predicate
196
216
  # @param {{timeout?: number}=} options
197
217
  # @return {!Promise<!Target>}
@@ -147,7 +147,7 @@ class Puppeteer::BrowserRunner
147
147
  end
148
148
 
149
149
  private def wait_for_ws_endpoint(browser_process, timeout, preferred_revision)
150
- Timeout.timeout(timeout / 1000) do
150
+ Timeout.timeout(timeout / 1000.0) do
151
151
  loop do
152
152
  line = browser_process.stderr.readline
153
153
  /^DevTools listening on (ws:\/\/.*)$/.match(line) do |m|
@@ -29,8 +29,12 @@ module Puppeteer::ConcurrentRubyUtils
29
29
  Concurrent::Promises.future(&block)
30
30
  end
31
31
 
32
- def resolvable_future
33
- Concurrent::Promises.resolvable_future
32
+ def resolvable_future(&block)
33
+ future = Concurrent::Promises.resolvable_future
34
+ if block
35
+ block.call(future)
36
+ end
37
+ future
34
38
  end
35
39
  end
36
40
 
@@ -44,7 +44,9 @@ class Puppeteer::Connection
44
44
 
45
45
  @transport = transport
46
46
  @transport.on_message do |data|
47
- async_handle_message(JSON.parse(data))
47
+ message = JSON.parse(data)
48
+ sleep_before_handling_message(message)
49
+ async_handle_message(message)
48
50
  end
49
51
  @transport.on_close do |reason, code|
50
52
  handle_close(reason, code)
@@ -54,6 +56,16 @@ class Puppeteer::Connection
54
56
  @closed = false
55
57
  end
56
58
 
59
+ private def sleep_before_handling_message(message)
60
+ # Puppeteer doesn't handle any Network monitoring responses.
61
+ # So we don't have to sleep.
62
+ return if message['method']&.start_with?('Network.')
63
+
64
+ # For some reasons, sleeping a bit reduces trivial errors...
65
+ # 4ms is an interval of internal shared timer of WebKit.
66
+ sleep 0.004
67
+ end
68
+
57
69
  def self.from_session(session)
58
70
  session.connection
59
71
  end
@@ -97,17 +97,18 @@ class Puppeteer::DOMWorld
97
97
  document.S(selector)
98
98
  end
99
99
 
100
- class DocumentEvaluationError < StandardError; end
101
-
102
100
  private def evaluate_document
103
101
  # sometimes execution_context.evaluate_handle('document') returns null object.
104
102
  # D, [2020-04-24T02:17:51.023631 #220] DEBUG -- : RECV << {"id"=>20, "result"=>{"result"=>{"type"=>"object", "subtype"=>"null", "value"=>nil}}, "sessionId"=>"78E9CF1E14D81294E320E7C20E5CDE06"}
105
103
  # retry if so.
106
- 5.times do
107
- handle = execution_context.evaluate_handle('document')
108
- return handle if handle.is_a?(Puppeteer::ElementHandle)
104
+ Timeout.timeout(3) do
105
+ loop do
106
+ handle = execution_context.evaluate_handle('document')
107
+ return handle if handle.is_a?(Puppeteer::ElementHandle)
108
+ end
109
109
  end
110
- raise DocumentEvaluationError.new("'document' object cannot be evaluated as an Element")
110
+ rescue Timeout::Error
111
+ raise 'Bug of puppeteer-ruby...'
111
112
  end
112
113
 
113
114
  private def document
@@ -355,18 +356,15 @@ class Puppeteer::DOMWorld
355
356
  # await handle.dispose();
356
357
  # }
357
358
 
358
- # /**
359
- # * @param {string} selector
360
- # * @param {!Array<string>} values
361
- # * @return {!Promise<!Array<string>>}
362
- # */
363
- # async select(selector, ...values) {
364
- # const handle = await this.$(selector);
365
- # assert(handle, 'No node found for selector: ' + selector);
366
- # const result = await handle.select(...values);
367
- # await handle.dispose();
368
- # return result;
369
- # }
359
+ # @param selector [String]
360
+ # @return [Array<String>]
361
+ def select(selector, *values)
362
+ handle = S(selector)
363
+ result = handle.select(*values)
364
+ handle.dispose
365
+
366
+ result
367
+ end
370
368
 
371
369
  # @param selector [String]
372
370
  def tap(selector)
@@ -1,3 +1,7 @@
1
+ require_relative './element_handle/bounding_box'
2
+ require_relative './element_handle/box_model'
3
+ require_relative './element_handle/point'
4
+
1
5
  class Puppeteer::ElementHandle < Puppeteer::JSHandle
2
6
  include Puppeteer::IfPresent
3
7
  using Puppeteer::AsyncAwaitBehavior
@@ -51,29 +55,6 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
51
55
  sleep 0.16
52
56
  end
53
57
 
54
- class Point
55
- def initialize(x:, y:)
56
- @x = x
57
- @y = y
58
- end
59
-
60
- def +(other)
61
- Point.new(
62
- x: @x + other.x,
63
- y: @y + other.y,
64
- )
65
- end
66
-
67
- def /(num)
68
- Point.new(
69
- x: @x / num,
70
- y: @y / num,
71
- )
72
- end
73
-
74
- attr_reader :x, :y
75
- end
76
-
77
58
  class ElementNotVisibleError < StandardError
78
59
  def initialize
79
60
  super("Node is either not visible or not an HTMLElement")
@@ -103,15 +84,6 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
103
84
  quads.first.reduce(:+) / 4
104
85
  end
105
86
 
106
- # /**
107
- # * @return {!Promise<void|Protocol.DOM.getBoxModelReturnValue>}
108
- # */
109
- # _getBoxModel() {
110
- # return this._client.send('DOM.getBoxModel', {
111
- # objectId: this._remoteObject.objectId
112
- # }).catch(error => debugError(error));
113
- # }
114
-
115
87
  # @param quad [Array<number>]
116
88
  # @return [Array<Point>]
117
89
  private def from_protocol_quad(quad)
@@ -155,74 +127,77 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
155
127
  click(delay: delay, button: button, click_count: click_count)
156
128
  end
157
129
 
158
- # /**
159
- # * @param {!Array<string>} values
160
- # * @return {!Promise<!Array<string>>}
161
- # */
162
- # async select(...values) {
163
- # for (const value of values)
164
- # assert(helper.isString(value), 'Values must be strings. Found value "' + value + '" of type "' + (typeof value) + '"');
165
- # return this.evaluate((element, values) => {
166
- # if (element.nodeName.toLowerCase() !== 'select')
167
- # throw new Error('Element is not a <select> element.');
168
-
169
- # const options = Array.from(element.options);
170
- # element.value = undefined;
171
- # for (const option of options) {
172
- # option.selected = values.includes(option.value);
173
- # if (option.selected && !element.multiple)
174
- # break;
175
- # }
176
- # element.dispatchEvent(new Event('input', { bubbles: true }));
177
- # element.dispatchEvent(new Event('change', { bubbles: true }));
178
- # return options.filter(option => option.selected).map(option => option.value);
179
- # }, values);
180
- # }
130
+ # @return [Array<String>]
131
+ def select(*values)
132
+ if nonstring = values.find { |value| !value.is_a?(String) }
133
+ raise ArgumentError.new("Values must be strings. Found value \"#{nonstring}\" of type \"#{nonstring.class}\"")
134
+ end
181
135
 
182
- # /**
183
- # * @param {!Array<string>} filePaths
184
- # */
185
- # async uploadFile(...filePaths) {
186
- # const isMultiple = await this.evaluate(element => element.multiple);
187
- # assert(filePaths.length <= 1 || isMultiple, 'Multiple file uploads only work with <input type=file multiple>');
188
- # // These imports are only needed for `uploadFile`, so keep them
189
- # // scoped here to avoid paying the cost unnecessarily.
190
- # const path = require('path');
191
- # const mime = require('mime-types');
192
- # const fs = require('fs');
193
- # const readFileAsync = helper.promisify(fs.readFile);
194
-
195
- # const promises = filePaths.map(filePath => readFileAsync(filePath));
196
- # const files = [];
197
- # for (let i = 0; i < filePaths.length; i++) {
198
- # const buffer = await promises[i];
199
- # const filePath = path.basename(filePaths[i]);
200
- # const file = {
201
- # name: filePath,
202
- # content: buffer.toString('base64'),
203
- # mimeType: mime.lookup(filePath),
204
- # };
205
- # files.push(file);
206
- # }
207
- # await this.evaluateHandle(async(element, files) => {
208
- # const dt = new DataTransfer();
209
- # for (const item of files) {
210
- # const response = await fetch(`data:${item.mimeType};base64,${item.content}`);
211
- # const file = new File([await response.blob()], item.name);
212
- # dt.items.add(file);
213
- # }
214
- # element.files = dt.files;
215
- # element.dispatchEvent(new Event('input', { bubbles: true }));
216
- # element.dispatchEvent(new Event('change', { bubbles: true }));
217
- # }, files);
218
- # }
136
+ fn = <<~JAVASCRIPT
137
+ (element, values) => {
138
+ if (element.nodeName.toLowerCase() !== 'select') {
139
+ throw new Error('Element is not a <select> element.');
140
+ }
141
+
142
+ const options = Array.from(element.options);
143
+ element.value = undefined;
144
+ for (const option of options) {
145
+ option.selected = values.includes(option.value);
146
+ if (option.selected && !element.multiple) {
147
+ break;
148
+ }
149
+ }
150
+ element.dispatchEvent(new Event('input', { bubbles: true }));
151
+ element.dispatchEvent(new Event('change', { bubbles: true }));
152
+ return options.filter(option => option.selected).map(option => option.value);
153
+ }
154
+ JAVASCRIPT
155
+ evaluate(fn, values)
156
+ end
157
+
158
+ # @param file_paths [Array<String>]
159
+ def upload_file(*file_paths)
160
+ is_multiple = evaluate("el => el.multiple")
161
+ if !is_multiple && file_paths.length >= 2
162
+ raise ArgumentError.new('Multiple file uploads only work with <input type=file multiple>')
163
+ end
164
+
165
+ if error_path = file_paths.find { |file_path| !File.exist?(file_path) }
166
+ raise ArgmentError.new("#{error_path} does not exist or is not readable")
167
+ end
168
+
169
+ backend_node_id = @remote_object.node_info(@client)["node"]["backendNodeId"]
170
+
171
+ # The zero-length array is a special case, it seems that DOM.setFileInputFiles does
172
+ # not actually update the files in that case, so the solution is to eval the element
173
+ # value to a new FileList directly.
174
+ if file_paths.empty?
175
+ fn = <<~JAVASCRIPT
176
+ (element) => {
177
+ element.files = new DataTransfer().files;
178
+
179
+ // Dispatch events for this case because it should behave akin to a user action.
180
+ element.dispatchEvent(new Event('input', { bubbles: true }));
181
+ element.dispatchEvent(new Event('change', { bubbles: true }));
182
+ }
183
+ JAVASCRIPT
184
+ await this.evaluate(fn)
185
+ else
186
+ @remote_object.set_file_input_files(@client, file_paths, backend_node_id)
187
+ end
188
+ end
189
+
190
+ def tap(&block)
191
+ return super(&block) if block
219
192
 
220
- def tap
221
193
  scroll_into_view_if_needed
222
194
  point = clickable_point
223
195
  @page.touchscreen.tap(point.x, point.y)
224
196
  end
225
197
 
198
+ async def async_tap
199
+ tap
200
+ end
226
201
 
227
202
  def focus
228
203
  evaluate('element => element.focus()')
@@ -260,89 +235,77 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
260
235
  press(key, delay: delay)
261
236
  end
262
237
 
263
- # /**
264
- # * @return {!Promise<?{x: number, y: number, width: number, height: number}>}
265
- # */
266
- # async boundingBox() {
267
- # const result = await this._getBoxModel();
268
-
269
- # if (!result)
270
- # return null;
271
-
272
- # const quad = result.model.border;
273
- # const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
274
- # const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
275
- # const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
276
- # const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
277
-
278
- # return {x, y, width, height};
279
- # }
280
-
281
- # /**
282
- # * @return {!Promise<?BoxModel>}
283
- # */
284
- # async boxModel() {
285
- # const result = await this._getBoxModel();
286
-
287
- # if (!result)
288
- # return null;
289
-
290
- # const {content, padding, border, margin, width, height} = result.model;
291
- # return {
292
- # content: this._fromProtocolQuad(content),
293
- # padding: this._fromProtocolQuad(padding),
294
- # border: this._fromProtocolQuad(border),
295
- # margin: this._fromProtocolQuad(margin),
296
- # width,
297
- # height
298
- # };
299
- # }
300
-
301
- # /**
302
- # *
303
- # * @param {!Object=} options
304
- # * @returns {!Promise<string|!Buffer>}
305
- # */
306
- # async screenshot(options = {}) {
307
- # let needsViewportReset = false;
308
-
309
- # let boundingBox = await this.boundingBox();
310
- # assert(boundingBox, 'Node is either not visible or not an HTMLElement');
311
-
312
- # const viewport = this._page.viewport();
313
-
314
- # if (viewport && (boundingBox.width > viewport.width || boundingBox.height > viewport.height)) {
315
- # const newViewport = {
316
- # width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
317
- # height: Math.max(viewport.height, Math.ceil(boundingBox.height)),
318
- # };
319
- # await this._page.setViewport(Object.assign({}, viewport, newViewport));
238
+ # @return [BoundingBox|nil]
239
+ def bounding_box
240
+ if_present(box_model) do |result_model|
241
+ quads = result_model.border
242
+
243
+ x = quads.map(&:x).min
244
+ y = quads.map(&:y).min
245
+ BoundingBox.new(
246
+ x: x,
247
+ y: y,
248
+ width: quads.map(&:x).max - x,
249
+ height: quads.map(&:y).max - y,
250
+ )
251
+ end
252
+ end
320
253
 
321
- # needsViewportReset = true;
322
- # }
254
+ # @return [BoxModel|nil]
255
+ def box_model
256
+ if_present(@remote_object.box_model(@client)) do |result|
257
+ BoxModel.new(result['model'])
258
+ end
259
+ end
323
260
 
324
- # await this._scrollIntoViewIfNeeded();
261
+ def screenshot(options = {})
262
+ needs_viewport_reset = false
325
263
 
326
- # boundingBox = await this.boundingBox();
327
- # assert(boundingBox, 'Node is either not visible or not an HTMLElement');
328
- # assert(boundingBox.width !== 0, 'Node has 0 width.');
329
- # assert(boundingBox.height !== 0, 'Node has 0 height.');
264
+ box = bounding_box
265
+ unless box
266
+ raise ElementNotVisibleError.new
267
+ end
330
268
 
331
- # const { layoutViewport: { pageX, pageY } } = await this._client.send('Page.getLayoutMetrics');
269
+ viewport = @page.viewport
270
+ if viewport && (box.width > viewport.width || box.height > viewport.height)
271
+ new_viewport = viewport.merge(
272
+ width: [viewport.width, box.width.to_i].min,
273
+ height: [viewport.height, box.height.to_i].min,
274
+ )
275
+ @page.viewport = new_viewport
332
276
 
333
- # const clip = Object.assign({}, boundingBox);
334
- # clip.x += pageX;
335
- # clip.y += pageY;
277
+ needs_viewport_reset = true
278
+ end
279
+ scroll_into_view_if_needed
336
280
 
337
- # const imageData = await this._page.screenshot(Object.assign({}, {
338
- # clip
339
- # }, options));
281
+ box = bounding_box
282
+ unless box
283
+ raise ElementNotVisibleError.new
284
+ end
285
+ if box.width == 0
286
+ raise 'Node has 0 width.'
287
+ end
288
+ if box.height == 0
289
+ raise 'Node has 0 height.'
290
+ end
340
291
 
341
- # if (needsViewportReset)
342
- # await this._page.setViewport(viewport);
292
+ layout_metrics = @client.send_message('Page.getLayoutMetrics')
293
+ page_x = layout_metrics["layoutViewport"]["pageX"]
294
+ page_y = layout_metrics["layoutViewport"]["pageY"]
295
+
296
+ clip = {
297
+ x: page_x + box.x,
298
+ y: page_y + box.y,
299
+ width: box.width,
300
+ height: box.height,
301
+ }
343
302
 
344
- # return imageData;
345
- # }
303
+ @page.screenshot({ clip: clip }.merge(options))
304
+ ensure
305
+ if needs_viewport_reset
306
+ @page.viewport = viewport
307
+ end
308
+ end
346
309
 
347
310
  # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
348
311
  # @param selector [String]
@@ -393,6 +356,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
393
356
  result
394
357
  end
395
358
 
359
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
360
+ # @param selector [String]
361
+ # @param page_function [String]
362
+ # @return [Object]
363
+ async def async_Seval(selector, page_function, *args)
364
+ Seval(selector, page_function, *args)
365
+ end
366
+
396
367
  # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
397
368
  # @param selector [String]
398
369
  # @param page_function [String]
@@ -408,6 +379,14 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
408
379
  result
409
380
  end
410
381
 
382
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
383
+ # @param selector [String]
384
+ # @param page_function [String]
385
+ # @return [Object]
386
+ async def async_SSeval(selector, page_function, *args)
387
+ SSeval(selector, page_function, *args)
388
+ end
389
+
411
390
  # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
412
391
  # @param expression [String]
413
392
  # @return [Array<ElementHandle>]