puppeteer-ruby 0.0.10

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 (161) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/stale.yml +16 -0
  4. data/.gitignore +19 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +302 -0
  7. data/.travis.yml +7 -0
  8. data/Dockerfile +6 -0
  9. data/Gemfile +6 -0
  10. data/README.md +54 -0
  11. data/Rakefile +1 -0
  12. data/bin/console +11 -0
  13. data/bin/setup +8 -0
  14. data/docker-compose.yml +15 -0
  15. data/docs/Puppeteer.html +2020 -0
  16. data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
  17. data/docs/Puppeteer/Browser.html +2148 -0
  18. data/docs/Puppeteer/BrowserContext.html +809 -0
  19. data/docs/Puppeteer/BrowserFetcher.html +214 -0
  20. data/docs/Puppeteer/BrowserRunner.html +914 -0
  21. data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
  22. data/docs/Puppeteer/CDPSession.html +813 -0
  23. data/docs/Puppeteer/CDPSession/Error.html +124 -0
  24. data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
  25. data/docs/Puppeteer/Connection.html +960 -0
  26. data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
  27. data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
  28. data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
  29. data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
  30. data/docs/Puppeteer/ConsoleMessage.html +565 -0
  31. data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
  32. data/docs/Puppeteer/DOMWorld.html +2219 -0
  33. data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
  34. data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
  35. data/docs/Puppeteer/DebugPrint.html +233 -0
  36. data/docs/Puppeteer/Device.html +470 -0
  37. data/docs/Puppeteer/Devices.html +139 -0
  38. data/docs/Puppeteer/ElementHandle.html +2542 -0
  39. data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
  40. data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
  41. data/docs/Puppeteer/ElementHandle/Point.html +492 -0
  42. data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
  43. data/docs/Puppeteer/EmulationManager.html +454 -0
  44. data/docs/Puppeteer/EventCallbackable.html +433 -0
  45. data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
  46. data/docs/Puppeteer/ExecutionContext.html +998 -0
  47. data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
  48. data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
  49. data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
  50. data/docs/Puppeteer/FileChooser.html +455 -0
  51. data/docs/Puppeteer/Frame.html +3677 -0
  52. data/docs/Puppeteer/FrameManager.html +2410 -0
  53. data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
  54. data/docs/Puppeteer/IfPresent.html +222 -0
  55. data/docs/Puppeteer/JSHandle.html +1352 -0
  56. data/docs/Puppeteer/Keyboard.html +1557 -0
  57. data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
  58. data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
  59. data/docs/Puppeteer/Launcher.html +237 -0
  60. data/docs/Puppeteer/Launcher/Base.html +385 -0
  61. data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
  62. data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
  63. data/docs/Puppeteer/Launcher/Chrome.html +669 -0
  64. data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
  65. data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
  66. data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
  67. data/docs/Puppeteer/LifecycleWatcher.html +834 -0
  68. data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
  69. data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
  70. data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
  71. data/docs/Puppeteer/Mouse.html +1105 -0
  72. data/docs/Puppeteer/Mouse/Button.html +136 -0
  73. data/docs/Puppeteer/NetworkManager.html +901 -0
  74. data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
  75. data/docs/Puppeteer/Page.html +5970 -0
  76. data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
  77. data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
  78. data/docs/Puppeteer/Page/ScriptTag.html +555 -0
  79. data/docs/Puppeteer/Page/StyleTag.html +448 -0
  80. data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
  81. data/docs/Puppeteer/RemoteObject.html +1087 -0
  82. data/docs/Puppeteer/Target.html +1336 -0
  83. data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
  84. data/docs/Puppeteer/Target/TargetInfo.html +729 -0
  85. data/docs/Puppeteer/TimeoutError.html +135 -0
  86. data/docs/Puppeteer/TimeoutSettings.html +496 -0
  87. data/docs/Puppeteer/TouchScreen.html +464 -0
  88. data/docs/Puppeteer/Viewport.html +837 -0
  89. data/docs/Puppeteer/WaitTask.html +637 -0
  90. data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
  91. data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
  92. data/docs/Puppeteer/WebSocket.html +673 -0
  93. data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
  94. data/docs/Puppeteer/WebSocketTransport.html +600 -0
  95. data/docs/Puppeteer/WebSocktTransportError.html +124 -0
  96. data/docs/_index.html +823 -0
  97. data/docs/class_list.html +51 -0
  98. data/docs/css/common.css +1 -0
  99. data/docs/css/full_list.css +58 -0
  100. data/docs/css/style.css +496 -0
  101. data/docs/file.README.html +123 -0
  102. data/docs/file_list.html +56 -0
  103. data/docs/frames.html +17 -0
  104. data/docs/index.html +123 -0
  105. data/docs/js/app.js +314 -0
  106. data/docs/js/full_list.js +216 -0
  107. data/docs/js/jquery.js +4 -0
  108. data/docs/method_list.html +4075 -0
  109. data/docs/top-level-namespace.html +126 -0
  110. data/lib/puppeteer.rb +200 -0
  111. data/lib/puppeteer/async_await_behavior.rb +38 -0
  112. data/lib/puppeteer/browser.rb +259 -0
  113. data/lib/puppeteer/browser_context.rb +90 -0
  114. data/lib/puppeteer/browser_fetcher.rb +6 -0
  115. data/lib/puppeteer/browser_runner.rb +161 -0
  116. data/lib/puppeteer/cdp_session.rb +100 -0
  117. data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
  118. data/lib/puppeteer/connection.rb +254 -0
  119. data/lib/puppeteer/console_message.rb +24 -0
  120. data/lib/puppeteer/debug_print.rb +20 -0
  121. data/lib/puppeteer/device.rb +12 -0
  122. data/lib/puppeteer/devices.rb +885 -0
  123. data/lib/puppeteer/dom_world.rb +484 -0
  124. data/lib/puppeteer/element_handle.rb +433 -0
  125. data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
  126. data/lib/puppeteer/element_handle/box_model.rb +19 -0
  127. data/lib/puppeteer/element_handle/point.rb +26 -0
  128. data/lib/puppeteer/emulation_manager.rb +46 -0
  129. data/lib/puppeteer/errors.rb +2 -0
  130. data/lib/puppeteer/event_callbackable.rb +88 -0
  131. data/lib/puppeteer/execution_context.rb +254 -0
  132. data/lib/puppeteer/file_chooser.rb +29 -0
  133. data/lib/puppeteer/frame.rb +286 -0
  134. data/lib/puppeteer/frame_manager.rb +378 -0
  135. data/lib/puppeteer/if_present.rb +18 -0
  136. data/lib/puppeteer/js_handle.rb +142 -0
  137. data/lib/puppeteer/keyboard.rb +183 -0
  138. data/lib/puppeteer/keyboard/key_description.rb +19 -0
  139. data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
  140. data/lib/puppeteer/launcher.rb +25 -0
  141. data/lib/puppeteer/launcher/base.rb +48 -0
  142. data/lib/puppeteer/launcher/browser_options.rb +41 -0
  143. data/lib/puppeteer/launcher/chrome.rb +211 -0
  144. data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
  145. data/lib/puppeteer/launcher/launch_options.rb +68 -0
  146. data/lib/puppeteer/lifecycle_watcher.rb +171 -0
  147. data/lib/puppeteer/mouse.rb +123 -0
  148. data/lib/puppeteer/network_manager.rb +122 -0
  149. data/lib/puppeteer/page.rb +1065 -0
  150. data/lib/puppeteer/page/screenshot_options.rb +78 -0
  151. data/lib/puppeteer/remote_object.rb +143 -0
  152. data/lib/puppeteer/target.rb +150 -0
  153. data/lib/puppeteer/timeout_settings.rb +15 -0
  154. data/lib/puppeteer/touch_screen.rb +43 -0
  155. data/lib/puppeteer/version.rb +3 -0
  156. data/lib/puppeteer/viewport.rb +54 -0
  157. data/lib/puppeteer/wait_task.rb +188 -0
  158. data/lib/puppeteer/web_socket.rb +122 -0
  159. data/lib/puppeteer/web_socket_transport.rb +49 -0
  160. data/puppeteer-ruby.gemspec +32 -0
  161. metadata +355 -0
@@ -0,0 +1,171 @@
1
+ require 'thread'
2
+
3
+ # https://github.com/puppeteer/puppeteer/blob/master/lib/LifecycleWatcher.js
4
+ class Puppeteer::LifecycleWatcher
5
+ include Puppeteer::IfPresent
6
+
7
+ class ExpectedLifecycle
8
+ PUPPETEER_TO_PROTOCOL_LIFECYCLE = {
9
+ 'load' => 'load',
10
+ 'domcontentloaded' => 'DOMContentLoaded',
11
+ 'networkidle0' => 'networkIdle',
12
+ 'networkidle2' => 'networkAlmostIdle',
13
+ }
14
+
15
+ def initialize(wait_until)
16
+ if wait_until.is_a?(Enumerable)
17
+ @wait_until = wait_until.map do |value|
18
+ unless PUPPETEER_TO_PROTOCOL_LIFECYCLE.has_key?(value.to_s)
19
+ raise ArgumentError.new("Unknown value for options.waitUntil: #{value}")
20
+ end
21
+ value.to_s
22
+ end
23
+ elsif wait_until.is_a?(String)
24
+ unless PUPPETEER_TO_PROTOCOL_LIFECYCLE.has_key?(wait_until)
25
+ raise ArgumentError.new("Unknown value for options.waitUntil: #{wait_until}")
26
+ end
27
+ @wait_until = [wait_until]
28
+ else
29
+ raise ArgumentError.new('wait_until should be a Array<String> or String')
30
+ end
31
+ end
32
+
33
+ private def expected_lifecycle
34
+ @expected_lifecycle ||= @wait_until.map do |value|
35
+ PUPPETEER_TO_PROTOCOL_LIFECYCLE[value]
36
+ end
37
+ end
38
+
39
+ # Check if navigation lifecycle has experienced the expected_lifecycle events.
40
+ #
41
+ # @param frame [Puppeteer::Frame]
42
+ def completed?(frame)
43
+ if expected_lifecycle.any? { |event| !frame.lifecycle_events.include?(event) }
44
+ return false
45
+ end
46
+ if frame.child_frames.any? { |child| !completed?(child) }
47
+ return false
48
+ end
49
+ true
50
+ end
51
+ end
52
+
53
+ class FrameDetachedError < StandardError
54
+ def initialize
55
+ super('Navigating frame was detached')
56
+ end
57
+ end
58
+ class TerminatedError < StandardError; end
59
+
60
+ # * @param {!Puppeteer.FrameManager} frameManager
61
+ # * @param {!Puppeteer.Frame} frame
62
+ # * @param {string|!Array<string>} waitUntil
63
+ # * @param {number} timeout
64
+ def initialize(frame_manager, frame, wait_until, timeout)
65
+ @expected_lifecycle = ExpectedLifecycle.new(wait_until)
66
+ @frame_manager = frame_manager
67
+ @frame = frame
68
+ @initial_loader_id = frame.loader_id
69
+ @timeout = timeout
70
+
71
+ @listener_ids = {}
72
+ @listener_ids['client'] = @frame_manager.client.add_event_listener('Events.CDPSession.Disconnected') do
73
+ terminate(TerminatedError.new('Navigation failed because browser has disconnected!'))
74
+ end
75
+ @listener_ids['frame_manager'] = [
76
+ @frame_manager.add_event_listener('Events.FrameManager.LifecycleEvent') do |_|
77
+ check_lifecycle_complete
78
+ end,
79
+ @frame_manager.add_event_listener('Events.FrameManager.FrameNavigatedWithinDocument', &method(:navigated_within_document)),
80
+ @frame_manager.add_event_listener('Events.FrameManager.FrameDetached', &method(:handle_frame_detached)),
81
+ ]
82
+ @listener_ids['network_manager'] = @frame_manager.network_manager.add_event_listener('Events.NetworkManager.Request', &method(:handle_request))
83
+
84
+ @same_document_navigation_promise = resolvable_future
85
+ @lifecycle_promise = resolvable_future
86
+ @new_document_navigation_promise = resolvable_future
87
+ @termination_promise = resolvable_future
88
+ check_lifecycle_complete
89
+ end
90
+
91
+ # @param [Puppeteer::Request] request
92
+ def handle_request(request)
93
+ return if request.frame != @frame || !request.navigation_request?
94
+ @navigation_request = request
95
+ end
96
+
97
+ # @param frame [Puppeteer::Frame]
98
+ def handle_frame_detached(frame)
99
+ if @frame == frame
100
+ @termination_promise.reject(FrameDetachedError.new)
101
+ return
102
+ end
103
+ check_lifecycle_complete
104
+ end
105
+
106
+ # @return [Puppeteer::Response]
107
+ def navigation_response
108
+ if_present(@navigation_request) do |request|
109
+ request.response
110
+ end
111
+ end
112
+
113
+ # @param error [TerminatedError]
114
+ private def terminate(error)
115
+ @termination_promise.reject(error)
116
+ end
117
+
118
+ attr_reader(
119
+ :same_document_navigation_promise,
120
+ :new_document_navigation_promise,
121
+ :lifecycle_promise,
122
+ )
123
+
124
+ def timeout_or_termination_promise
125
+ if @timeout > 0
126
+ future do
127
+ Timeout.timeout(@timeout / 1000.0) do
128
+ @termination_promise.value!
129
+ end
130
+ rescue Timeout::Error
131
+ raise Puppeteer::FrameManager::NavigationError.new("Navigation timeout of #{@timeout}ms exceeded")
132
+ end
133
+ else
134
+ @termination_promise
135
+ end
136
+ end
137
+
138
+ # @param frame [Puppeteer::Frame]
139
+ private def navigated_within_document(frame)
140
+ return if frame != @frame
141
+ @has_same_document_navigation = true
142
+ check_lifecycle_complete
143
+ end
144
+
145
+ private def check_lifecycle_complete
146
+ # We expect navigation to commit.
147
+ return unless @expected_lifecycle.completed?(@frame)
148
+ @lifecycle_promise.fulfill(true) if @lifecycle_promise.pending?
149
+ if @frame.loader_id == @initial_loader_id && !@has_same_document_navigation
150
+ return
151
+ end
152
+ if @has_same_document_navigation && @same_document_navigation_promise.pending?
153
+ @same_document_navigation_promise.fulfill(true)
154
+ end
155
+ if @frame.loader_id != @initial_loader_id && @new_document_navigation_promise.pending?
156
+ @new_document_navigation_promise.fulfill(true)
157
+ end
158
+ end
159
+
160
+ def dispose
161
+ if_present(@listener_ids['client']) do |id|
162
+ @frame_manager.client.remove_event_listener(id)
163
+ end
164
+ if_present(@listener_ids['frame_manager']) do |ids|
165
+ @frame_manager.remove_event_listener(*ids)
166
+ end
167
+ if_present(@listener_ids['network_manager']) do |id|
168
+ @frame_manager.network_manager.remove_event_listener(id)
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,123 @@
1
+ class Puppeteer::Mouse
2
+ using Puppeteer::AsyncAwaitBehavior
3
+
4
+ module Button
5
+ NONE = 'none'
6
+ LEFT = 'left'
7
+ RIGHT = 'right'
8
+ MIDDLE = 'middle'
9
+ end
10
+
11
+ # @param {Puppeteer.CDPSession} client
12
+ # @param keyboard [Puppeteer::Keyboard]
13
+ def initialize(client, keyboard)
14
+ @client = client
15
+ @keyboard = keyboard
16
+
17
+ @x = 0
18
+ @y = 0
19
+ @button = Button::NONE
20
+ end
21
+
22
+ # @param x [number]
23
+ # @param y [number]
24
+ # @param steps [number]
25
+ def move(x, y, steps: nil)
26
+ move_steps = (steps || 1).to_i
27
+
28
+ from_x = @x
29
+ from_y = @y
30
+ @x = x
31
+ @y = y
32
+
33
+ return if move_steps <= 0
34
+
35
+ move_steps.times do |i|
36
+ n = i + 1
37
+ @client.send_message('Input.dispatchMouseEvent',
38
+ type: 'mouseMoved',
39
+ button: @button,
40
+ x: from_x + (@x - from_x) * n / move_steps,
41
+ y: from_y + (@y - from_y) * n / move_steps,
42
+ modifiers: @keyboard.modifiers,
43
+ )
44
+ end
45
+ end
46
+
47
+ # @param x [number]
48
+ # @param y [number]
49
+ # @param steps [number]
50
+ # @return [Future]
51
+ async def async_move(x, y, steps: nil)
52
+ move(x, y, steps: steps)
53
+ end
54
+
55
+ # @param x [number]
56
+ # @param y [number]
57
+ # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
58
+ def click(x, y, delay: nil, button: nil, click_count: nil)
59
+ # await_all(async_move, async_down, async_up) often breaks the order of CDP commands.
60
+ # D, [2020-04-15T17:09:47.895895 #88683] DEBUG -- : RECV << {"id"=>23, "result"=>{"layoutViewport"=>{"pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667}, "visualViewport"=>{"offsetX"=>0, "offsetY"=>0, "pageX"=>0, "pageY"=>1, "clientWidth"=>375, "clientHeight"=>667, "scale"=>1, "zoom"=>1}, "contentSize"=>{"x"=>0, "y"=>0, "width"=>375, "height"=>2007}}, "sessionId"=>"0B09EA5E18DEE403E525B3E7FCD7E225"}
61
+ # D, [2020-04-15T17:09:47.898422 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseReleased","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":24}
62
+ # D, [2020-04-15T17:09:47.899711 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mousePressed","button":"left","x":0,"y":0,"modifiers":0,"clickCount":1},"id":25}
63
+ # D, [2020-04-15T17:09:47.900237 #88683] DEBUG -- : SEND >> {"sessionId":"0B09EA5E18DEE403E525B3E7FCD7E225","method":"Input.dispatchMouseEvent","params":{"type":"mouseMoved","button":"left","x":187,"y":283,"modifiers":0},"id":26}
64
+ # So we execute move in advance.
65
+ move(x, y)
66
+ if delay
67
+ down(button: button, click_count: click_count)
68
+ sleep(delay / 1000.0)
69
+ up(button: button, click_count: click_count)
70
+ else
71
+ await_all(
72
+ async_down(button: button, click_count: click_count),
73
+ async_up(button: button, click_count: click_count),
74
+ )
75
+ end
76
+ end
77
+
78
+ # @param x [number]
79
+ # @param y [number]
80
+ # @param {!{delay?: number, button?: "left"|"right"|"middle", clickCount?: number}=} options
81
+ # @return [Future]
82
+ async def async_click(x, y, delay: nil, button: nil, click_count: nil)
83
+ click(x, y, delay: delay, button: button, click_count: click_count)
84
+ end
85
+
86
+ # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
87
+ def down(button: nil, click_count: nil)
88
+ @button = button || Button::LEFT
89
+ @client.send_message('Input.dispatchMouseEvent',
90
+ type: 'mousePressed',
91
+ button: @button,
92
+ x: @x,
93
+ y: @y,
94
+ modifiers: @keyboard.modifiers,
95
+ clickCount: click_count || 1,
96
+ )
97
+ end
98
+
99
+ # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
100
+ # @return [Future]
101
+ async def async_down(button: nil, click_count: nil)
102
+ down(button: button, click_count: click_count)
103
+ end
104
+
105
+ # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
106
+ def up(button: nil, click_count: nil)
107
+ @button = Button::NONE
108
+ @client.send_message('Input.dispatchMouseEvent',
109
+ type: 'mouseReleased',
110
+ button: button || Button::LEFT,
111
+ x: @x,
112
+ y: @y,
113
+ modifiers: @keyboard.modifiers,
114
+ clickCount: click_count || 1,
115
+ )
116
+ end
117
+
118
+ # @param {!{button?: "left"|"right"|"middle", clickCount?: number}=} options
119
+ # @return [Future]
120
+ async def async_up(button: nil, click_count: nil)
121
+ up(button: button, click_count: click_count)
122
+ end
123
+ end
@@ -0,0 +1,122 @@
1
+ class Puppeteer::NetworkManager
2
+ include Puppeteer::EventCallbackable
3
+
4
+ class Credentials
5
+ # @param username [String|NilClass]
6
+ # @param password [String|NilClass]
7
+ def initialize(username:, password:)
8
+ @username = username
9
+ @password = password
10
+ end
11
+ attr_reader :username, :password
12
+ end
13
+
14
+ # @param {!Puppeteer.CDPSession} client
15
+ # @param {boolean} ignoreHTTPSErrors
16
+ # @param {!Puppeteer.FrameManager} frameManager
17
+ def initialize(client, ignore_https_errors, frame_manager)
18
+ @client = client
19
+ @ignore_https_errors = ignore_https_errors
20
+ @frame_manager = frame_manager
21
+
22
+ # @type {!Map<string, !Request>}
23
+ @request_id_to_request = {}
24
+
25
+ # @type {!Map<string, !Protocol.Network.requestWillBeSentPayload>}
26
+ @request_id_to_request_with_be_sent_event
27
+
28
+ @extra_http_headers = {}
29
+
30
+ @offline = false
31
+
32
+ # /** @type {!Set<string>} */
33
+ # this._attemptedAuthentications = new Set();
34
+ @user_request_interception_enabled = false
35
+ @protocol_request_interception_enabled = false
36
+ @user_cache_disabled = false
37
+ # /** @type {!Map<string, string>} */
38
+ # this._requestIdToInterceptionId = new Map();
39
+ end
40
+
41
+ def init
42
+ @client.send_message('Network.enable')
43
+ if @ignore_https_errors
44
+ @client.send_message('Security.setIgnoreCertificateErrors', ignore: true)
45
+ end
46
+ end
47
+
48
+ # @param username [String|NilClass]
49
+ # @param password [String|NilClass]
50
+ def authenticate(username:, password:)
51
+ @credentials = Credentials.new(username: username, password: password)
52
+ update_protocol_request_interception
53
+ end
54
+
55
+ # @param {!Object<string, string>} extraHTTPHeaders
56
+ def extra_http_headers=(headers)
57
+ new_extra_http_headers = {}
58
+ headers.each do |key, value|
59
+ unless value.is_a?(String)
60
+ raise ArgumentError.new("Expected value of header \"#{key}\" to be String, but \"#{value}\" is found.")
61
+ end
62
+ new_extra_http_headers[key.downcase] = value
63
+ end
64
+ @extra_http_headers = new_extra_http_headers
65
+ @client.send_message('Network.setExtraHTTPHeaders', headers: new_extra_http_headers)
66
+ end
67
+
68
+ # @return {!Object<string, string>}
69
+ def extra_http_headers
70
+ @extra_http_headers.dup
71
+ end
72
+
73
+ # @param value [TrueClass|FalseClass]
74
+ def offline_mode=(value)
75
+ return if @offline == value
76
+ @offline = value
77
+ @client.send_message('Network.emulateNetworkConditions',
78
+ offline: @offline,
79
+ # values of 0 remove any active throttling. crbug.com/456324#c9
80
+ latency: 0,
81
+ downloadThroughput: -1,
82
+ uploadThroughput: -1,
83
+ )
84
+ end
85
+
86
+ # @param user_agent [String]
87
+ def user_agent=(user_agent)
88
+ @client.send_message('Network.setUserAgentOverride', userAgent: user_agent)
89
+ end
90
+
91
+ def cache_enabled=(enabled)
92
+ @user_cache_disabled = !enabled
93
+ update_protocol_cache_disabled
94
+ end
95
+
96
+ def request_interception=(enabled)
97
+ @user_request_interception_enabled = enabled
98
+ update_protocol_request_interception
99
+ end
100
+
101
+ private def update_protocol_request_interception
102
+ enabled = @user_request_interception_enabled || !@credentials.nil?
103
+ return if @protocol_request_interception_enabled == enabled
104
+ @protocol_request_interception_enabled = enabled
105
+
106
+ if enabled
107
+ update_protocol_cache_disabled
108
+ @client.send_message('Fetch.enable',
109
+ handleAuthRequests: true,
110
+ patterns: [{ urlPattern: '*' }],
111
+ )
112
+ else
113
+ update_protocol_cache_disabled
114
+ @client.async_send_message('Fetch.disable')
115
+ end
116
+ end
117
+
118
+ private def update_protocol_cache_disabled
119
+ cache_disabled = @user_cache_disabled || @protocol_request_interception_enabled
120
+ @client.send_message('Network.setCacheDisabled', cacheDisabled: cache_disabled)
121
+ end
122
+ end
@@ -0,0 +1,1065 @@
1
+ require 'base64'
2
+
3
+ require_relative './page/screenshot_options'
4
+
5
+ class Puppeteer::Page
6
+ include Puppeteer::EventCallbackable
7
+ include Puppeteer::IfPresent
8
+ using Puppeteer::AsyncAwaitBehavior
9
+
10
+ # @param {!Puppeteer.CDPSession} client
11
+ # @param {!Puppeteer.Target} target
12
+ # @param {boolean} ignoreHTTPSErrors
13
+ # @param {?Puppeteer.Viewport} defaultViewport
14
+ # @param {!Puppeteer.TaskQueue} screenshotTaskQueue
15
+ # @return {!Promise<!Page>}
16
+ def self.create(client, target, ignore_https_errors, default_viewport, screenshot_task_queue)
17
+ page = Puppeteer::Page.new(client, target, ignore_https_errors, screenshot_task_queue)
18
+ page.init
19
+ if default_viewport
20
+ page.viewport = default_viewport
21
+ end
22
+ page
23
+ end
24
+
25
+ # @param {!Puppeteer.CDPSession} client
26
+ # @param {!Puppeteer.Target} target
27
+ # @param {boolean} ignoreHTTPSErrors
28
+ # @param {!Puppeteer.TaskQueue} screenshotTaskQueue
29
+ def initialize(client, target, ignore_https_errors, screenshot_task_queue)
30
+ @closed = false
31
+ @client = client
32
+ @target = target
33
+ @keyboard = Puppeteer::Keyboard.new(client)
34
+ @mouse = Puppeteer::Mouse.new(client, @keyboard)
35
+ @timeout_settings = Puppeteer::TimeoutSettings.new
36
+ @touchscreen = Puppeteer::TouchScreen.new(client, @keyboard)
37
+ # @accessibility = Accessibility.new(client)
38
+ @frame_manager = Puppeteer::FrameManager.new(client, self, ignore_https_errors, @timeout_settings)
39
+ @emulation_manager = Puppeteer::EmulationManager.new(client)
40
+ # @tracing = Tracing.new(client)
41
+ @page_bindings = {}
42
+ # @coverage = Coverage.new(client)
43
+ @javascript_enabled = true
44
+ @screenshot_task_queue = screenshot_task_queue
45
+
46
+ @workers = {}
47
+ @client.on_event 'Target.attachedToTarget' do |event|
48
+ if event['targetInfo']['type'] != 'worker'
49
+ # If we don't detach from service workers, they will never die.
50
+ await @client.send_message('Target.detachFromTarget', sessionId: event['sessionId'])
51
+ next
52
+ end
53
+
54
+ session = Puppeteer::Connection.from_session(@client).session(event['sessionId']) # rubocop:disable Lint/UselessAssignment
55
+ # const worker = new Worker(session, event.targetInfo.url, this._addConsoleMessage.bind(this), this._handleException.bind(this));
56
+ # this._workers.set(event.sessionId, worker);
57
+ # this.emit(Events.Page.WorkerCreated, worker);
58
+ end
59
+ @client.on_event 'Target.detachedFromTarget' do |event|
60
+ session_id = event['sessionId']
61
+ worker = @workers[session_id]
62
+ next unless worker
63
+
64
+ emit_event('Events.Page.WorkerDestroyed', worker)
65
+ @workers.delete(session_id)
66
+ end
67
+
68
+ @frame_manager.on_event 'Events.FrameManager.FrameAttached' do |event|
69
+ emit_event 'Events.Page.FrameAttached', event
70
+ end
71
+ @frame_manager.on_event 'Events.FrameManager.FrameDetached' do |event|
72
+ emit_event 'Events.Page.FrameDetached', event
73
+ end
74
+ @frame_manager.on_event 'Events.FrameManager.FrameNavigated' do |event|
75
+ emit_event 'Events.Page.FrameNavigated', event
76
+ end
77
+
78
+ network_manager = @frame_manager.network_manager
79
+ network_manager.on_event 'Events.NetworkManager.Request' do |event|
80
+ emit_event 'Events.Page.Request', event
81
+ end
82
+ network_manager.on_event 'Events.NetworkManager.Response' do |event|
83
+ emit_event 'Events.Page.Response', event
84
+ end
85
+ network_manager.on_event 'Events.NetworkManager.RequestFailed' do |event|
86
+ emit_event 'Events.Page.RequestFailed', event
87
+ end
88
+ network_manager.on_event 'Events.NetworkManager.RequestFinished' do |event|
89
+ emit_event 'Events.Page.RequestFinished', event
90
+ end
91
+ @file_chooser_interception_is_disabled = false
92
+ @file_chooser_interceptors = Set.new
93
+
94
+ @client.on_event 'Page.domContentEventFired' do |event|
95
+ emit_event 'Events.Page.DOMContentLoaded'
96
+ end
97
+ @client.on_event 'Page.loadEventFired' do |event|
98
+ emit_event 'Events.Page.Load'
99
+ end
100
+ # client.on('Runtime.consoleAPICalled', event => this._onConsoleAPI(event));
101
+ # client.on('Runtime.bindingCalled', event => this._onBindingCalled(event));
102
+ # client.on('Page.javascriptDialogOpening', event => this._onDialog(event));
103
+ # client.on('Runtime.exceptionThrown', exception => this._handleException(exception.exceptionDetails));
104
+ # client.on('Inspector.targetCrashed', event => this._onTargetCrashed());
105
+ # client.on('Performance.metrics', event => this._emitMetrics(event));
106
+ @client.on_event 'Log.entryAdded' do |event|
107
+ handle_log_entry_added(event)
108
+ end
109
+ @client.on_event 'Page.fileChooserOpened' do |event|
110
+ handle_file_chooser(event)
111
+ end
112
+ @target.is_closed_promise.then do
113
+ emit_event 'Events.Page.Close'
114
+ @closed = true
115
+ end
116
+ end
117
+
118
+ def init
119
+ await_all(
120
+ @frame_manager.async_init,
121
+ @client.async_send_message('Target.setAutoAttach', autoAttach: true, waitForDebuggerOnStart: false, flatten: true),
122
+ @client.async_send_message('Performance.enable'),
123
+ @client.async_send_message('Log.enable'),
124
+ )
125
+ end
126
+
127
+ def handle_file_chooser(event)
128
+ return if @file_chooser_interceptors.empty?
129
+
130
+ frame = @frame_manager.frame(event['frameId'])
131
+ context = frame.execution_context
132
+ element = context.adopt_backend_node_id(event['backendNodeId'])
133
+ interceptors = @file_chooser_interceptors.to_a
134
+ @file_chooser_interceptors.clear
135
+ file_chooser = Puppeteer::FileChooser.new(element, event)
136
+ interceptors.each do |promise|
137
+ promise.fulfill(file_chooser)
138
+ end
139
+ end
140
+
141
+ class FileChooserTimeoutError < StandardError
142
+ def initialize(timeout:)
143
+ super("waiting for filechooser failed: timeout #{timeout}ms exceeded")
144
+ end
145
+ end
146
+
147
+ # @param timeout [Integer]
148
+ # @return [Puppeteer::FileChooser]
149
+ def wait_for_file_chooser(timeout: nil)
150
+ if @file_chooser_interceptors.empty?
151
+ @client.send_message('Page.setInterceptFileChooserDialog', enabled: true)
152
+ end
153
+
154
+ option_timeout = timeout || @timeout_settings.timeout
155
+ promise = resolvable_future
156
+ @file_chooser_interceptors << promise
157
+
158
+ begin
159
+ Timeout.timeout(option_timeout / 1000.0) do
160
+ promise.value!
161
+ end
162
+ rescue Timeout::Error
163
+ raise FileChooserTimeoutError.new(timeout: option_timeout)
164
+ ensure
165
+ @file_chooser_interceptors.delete(promise)
166
+ end
167
+ end
168
+
169
+ # @param timeout [Integer]
170
+ # @return [Future<Puppeteer::FileChooser>]
171
+ async def async_wait_for_file_chooser(timeout: nil)
172
+ wait_for_file_chooser(timeout: timeout)
173
+ end
174
+
175
+
176
+ # /**
177
+ # * @param {!{longitude: number, latitude: number, accuracy: (number|undefined)}} options
178
+ # */
179
+ # async setGeolocation(options) {
180
+ # const { longitude, latitude, accuracy = 0} = options;
181
+ # if (longitude < -180 || longitude > 180)
182
+ # throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`);
183
+ # if (latitude < -90 || latitude > 90)
184
+ # throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`);
185
+ # if (accuracy < 0)
186
+ # throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`);
187
+ # await this._client.send('Emulation.setGeolocationOverride', {longitude, latitude, accuracy});
188
+ # }
189
+
190
+ attr_reader :javascript_enabled, :target
191
+
192
+ def browser
193
+ @target.browser
194
+ end
195
+
196
+ def browser_context
197
+ @target.browser_context
198
+ end
199
+
200
+ class TargetCrashedError < StandardError; end
201
+
202
+ private def handle_target_crashed
203
+ emit_event 'error', TargetCrashedError.new('Page crashed!')
204
+ end
205
+
206
+ private def handle_log_entry_added(event)
207
+ entry = event['entry']
208
+ level = entry['level']
209
+ text = entry['text']
210
+ source = entry['source']
211
+ url = entry['url']
212
+ line_number = entry['lineNumber']
213
+
214
+ if_present(entry['args']) do |args|
215
+ args.map do |arg|
216
+ Puppeteer::RemoteObject.new(arg).async_release(@client)
217
+ end
218
+ end
219
+ if source != 'worker'
220
+ console_message_location = Puppeteer::ConsoleMessage::Location.new(
221
+ url: url,
222
+ line_number: line_number,
223
+ )
224
+ emit_event('Events.Page.Console',
225
+ Puppeteer::ConsoleMessage.new(level, text, [], console_message_location))
226
+ end
227
+ end
228
+
229
+ def main_frame
230
+ @frame_manager.main_frame
231
+ end
232
+
233
+ attr_reader :keyboard, :touch_screen, :coverage, :accessibility
234
+
235
+ def frames
236
+ @frame_manager.frames
237
+ end
238
+
239
+ def workers
240
+ @workers.values
241
+ end
242
+
243
+ # @param value [Bool]
244
+ def request_interception=(value)
245
+ @frame_manager.network_manager.request_interception = value
246
+ end
247
+
248
+ def offline_mode=(enabled)
249
+ @frame_manager.network_manager.offline_mode = enabled
250
+ end
251
+
252
+ # @param {number} timeout
253
+ def default_navigation_timeout=(timeout)
254
+ @timeout_settings.default_navigation_timeout = timeout
255
+ end
256
+
257
+ # @param {number} timeout
258
+ def default_timeout=(timeout)
259
+ @timeout_settings.default_timeout = timeout
260
+ end
261
+
262
+ # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
263
+ # @param {string} selector
264
+ # @return {!Promise<?Puppeteer.ElementHandle>}
265
+ def S(selector)
266
+ main_frame.S(selector)
267
+ end
268
+
269
+ # `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
270
+ # @param {string} selector
271
+ # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
272
+ def SS(selector)
273
+ main_frame.SS(selector)
274
+ end
275
+
276
+ # @param {Function|string} pageFunction
277
+ # @param {!Array<*>} args
278
+ # @return {!Promise<!Puppeteer.JSHandle>}
279
+ def evaluate_handle(page_function, *args)
280
+ context = main_frame.execution_context
281
+ context.evaluate_handle(page_function, *args)
282
+ end
283
+
284
+ # @param {!Puppeteer.JSHandle} prototypeHandle
285
+ # @return {!Promise<!Puppeteer.JSHandle>}
286
+ def query_objects(prototype_handle)
287
+ context = main_frame.execution_context
288
+ context.query_objects(prototype_handle)
289
+ end
290
+
291
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
292
+ # @param selector [String]
293
+ # @param page_function [String]
294
+ # @return [Object]
295
+ def Seval(selector, page_function, *args)
296
+ main_frame.Seval(selector, page_function, *args)
297
+ end
298
+
299
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
300
+ # @param selector [String]
301
+ # @param page_function [String]
302
+ # @return [Future]
303
+ async def async_Seval(selector, page_function, *args)
304
+ Seval(selector, page_function, *args)
305
+ end
306
+
307
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
308
+ # @param selector [String]
309
+ # @param page_function [String]
310
+ # @return [Object]
311
+ def SSeval(selector, page_function, *args)
312
+ main_frame.SSeval(selector, page_function, *args)
313
+ end
314
+
315
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
316
+ # @param selector [String]
317
+ # @param page_function [String]
318
+ # @return [Future]
319
+ async def async_SSeval(selector, page_function, *args)
320
+ SSeval(selector, page_function, *args)
321
+ end
322
+
323
+ # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
324
+ # @param {string} expression
325
+ # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
326
+ def Sx(expression)
327
+ main_frame.Sx(expression)
328
+ end
329
+
330
+ # /**
331
+ # * @param {!Array<string>} urls
332
+ # * @return {!Promise<!Array<Network.Cookie>>}
333
+ # */
334
+ # async cookies(...urls) {
335
+ # return (await this._client.send('Network.getCookies', {
336
+ # urls: urls.length ? urls : [this.url()]
337
+ # })).cookies;
338
+ # }
339
+
340
+ # /**
341
+ # * @param {Array<Protocol.Network.deleteCookiesParameters>} cookies
342
+ # */
343
+ # async deleteCookie(...cookies) {
344
+ # const pageURL = this.url();
345
+ # for (const cookie of cookies) {
346
+ # const item = Object.assign({}, cookie);
347
+ # if (!cookie.url && pageURL.startsWith('http'))
348
+ # item.url = pageURL;
349
+ # await this._client.send('Network.deleteCookies', item);
350
+ # }
351
+ # }
352
+
353
+ # /**
354
+ # * @param {Array<Network.CookieParam>} cookies
355
+ # */
356
+ # async setCookie(...cookies) {
357
+ # const pageURL = this.url();
358
+ # const startsWithHTTP = pageURL.startsWith('http');
359
+ # const items = cookies.map(cookie => {
360
+ # const item = Object.assign({}, cookie);
361
+ # if (!item.url && startsWithHTTP)
362
+ # item.url = pageURL;
363
+ # assert(item.url !== 'about:blank', `Blank page can not have cookie "${item.name}"`);
364
+ # assert(!String.prototype.startsWith.call(item.url || '', 'data:'), `Data URL page can not have cookie "${item.name}"`);
365
+ # return item;
366
+ # });
367
+ # await this.deleteCookie(...items);
368
+ # if (items.length)
369
+ # await this._client.send('Network.setCookies', { cookies: items });
370
+ # }
371
+
372
+ class ScriptTag
373
+ # @param {!{content?: string, path?: string, type?: string, url?: string}} options
374
+ def initialize(content: nil, path: nil, type: nil, url: nil)
375
+ @content = content
376
+ @path = path
377
+ @type = type
378
+ @url = url
379
+ end
380
+ attr_reader :content, :path, :type, :url
381
+ end
382
+
383
+ # @param style_tag [Puppeteer::Page::ScriptTag]
384
+ # @return {!Promise<!ElementHandle>}
385
+ def add_script_tag(script_tag)
386
+ main_frame.add_script_tag(script_tag)
387
+ end
388
+
389
+ class StyleTag
390
+ # @param {!{content?: string, path?: string, url?: string}} options
391
+ def initialize(content: nil, path: nil, url: nil)
392
+ @content = content
393
+ @path = path
394
+ @url = url
395
+ end
396
+ attr_reader :content, :path, :url
397
+ end
398
+
399
+ # @param style_tag [Puppeteer::Page::StyleTag]
400
+ # @return {!Promise<!ElementHandle>}
401
+ def add_style_tag(style_tag)
402
+ main_frame.add_style_tag(style_tag)
403
+ end
404
+
405
+ # /**
406
+ # * @param {string} name
407
+ # * @param {Function} puppeteerFunction
408
+ # */
409
+ # async exposeFunction(name, puppeteerFunction) {
410
+ # if (this._pageBindings.has(name))
411
+ # throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
412
+ # this._pageBindings.set(name, puppeteerFunction);
413
+
414
+ # const expression = helper.evaluationString(addPageBinding, name);
415
+ # await this._client.send('Runtime.addBinding', {name: name});
416
+ # await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: expression});
417
+ # await Promise.all(this.frames().map(frame => frame.evaluate(expression).catch(debugError)));
418
+
419
+ # function addPageBinding(bindingName) {
420
+ # const binding = window[bindingName];
421
+ # window[bindingName] = (...args) => {
422
+ # const me = window[bindingName];
423
+ # let callbacks = me['callbacks'];
424
+ # if (!callbacks) {
425
+ # callbacks = new Map();
426
+ # me['callbacks'] = callbacks;
427
+ # }
428
+ # const seq = (me['lastSeq'] || 0) + 1;
429
+ # me['lastSeq'] = seq;
430
+ # const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject}));
431
+ # binding(JSON.stringify({name: bindingName, seq, args}));
432
+ # return promise;
433
+ # };
434
+ # }
435
+ # }
436
+
437
+ # @param username [String?]
438
+ # @param password [String?]
439
+ def authenticate(username: nil, password: nil)
440
+ @frame_manager.network_manager.authenticate(username: username, password: password)
441
+ end
442
+
443
+ # @param headers [Hash]
444
+ def extra_http_headers=(headers)
445
+ @frame_manager.network_manager.extra_http_headers = headers
446
+ end
447
+
448
+ # @param user_agent [String]
449
+ def user_agent=(user_agent)
450
+ @frame_manager.network_manager.user_agent = user_agent
451
+ end
452
+
453
+ # /**
454
+ # * @return {!Promise<!Metrics>}
455
+ # */
456
+ # async metrics() {
457
+ # const response = await this._client.send('Performance.getMetrics');
458
+ # return this._buildMetricsObject(response.metrics);
459
+ # }
460
+
461
+ # /**
462
+ # * @param {!Protocol.Performance.metricsPayload} event
463
+ # */
464
+ # _emitMetrics(event) {
465
+ # this.emit(Events.Page.Metrics, {
466
+ # title: event.title,
467
+ # metrics: this._buildMetricsObject(event.metrics)
468
+ # });
469
+ # }
470
+
471
+ # /**
472
+ # * @param {?Array<!Protocol.Performance.Metric>} metrics
473
+ # * @return {!Metrics}
474
+ # */
475
+ # _buildMetricsObject(metrics) {
476
+ # const result = {};
477
+ # for (const metric of metrics || []) {
478
+ # if (supportedMetrics.has(metric.name))
479
+ # result[metric.name] = metric.value;
480
+ # }
481
+ # return result;
482
+ # }
483
+
484
+ # /**
485
+ # * @param {!Protocol.Runtime.ExceptionDetails} exceptionDetails
486
+ # */
487
+ # _handleException(exceptionDetails) {
488
+ # const message = helper.getExceptionMessage(exceptionDetails);
489
+ # const err = new Error(message);
490
+ # err.stack = ''; // Don't report clientside error with a node stack attached
491
+ # this.emit(Events.Page.PageError, err);
492
+ # }
493
+
494
+ # /**
495
+ # * @param {!Protocol.Runtime.consoleAPICalledPayload} event
496
+ # */
497
+ # async _onConsoleAPI(event) {
498
+ # if (event.executionContextId === 0) {
499
+ # // DevTools protocol stores the last 1000 console messages. These
500
+ # // messages are always reported even for removed execution contexts. In
501
+ # // this case, they are marked with executionContextId = 0 and are
502
+ # // reported upon enabling Runtime agent.
503
+ # //
504
+ # // Ignore these messages since:
505
+ # // - there's no execution context we can use to operate with message
506
+ # // arguments
507
+ # // - these messages are reported before Puppeteer clients can subscribe
508
+ # // to the 'console'
509
+ # // page event.
510
+ # //
511
+ # // @see https://github.com/puppeteer/puppeteer/issues/3865
512
+ # return;
513
+ # }
514
+ # const context = this._frameManager.executionContextById(event.executionContextId);
515
+ # const values = event.args.map(arg => createJSHandle(context, arg));
516
+ # this._addConsoleMessage(event.type, values, event.stackTrace);
517
+ # }
518
+
519
+ # /**
520
+ # * @param {!Protocol.Runtime.bindingCalledPayload} event
521
+ # */
522
+ # async _onBindingCalled(event) {
523
+ # const {name, seq, args} = JSON.parse(event.payload);
524
+ # let expression = null;
525
+ # try {
526
+ # const result = await this._pageBindings.get(name)(...args);
527
+ # expression = helper.evaluationString(deliverResult, name, seq, result);
528
+ # } catch (error) {
529
+ # if (error instanceof Error)
530
+ # expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
531
+ # else
532
+ # expression = helper.evaluationString(deliverErrorValue, name, seq, error);
533
+ # }
534
+ # this._client.send('Runtime.evaluate', { expression, contextId: event.executionContextId }).catch(debugError);
535
+
536
+ # /**
537
+ # * @param {string} name
538
+ # * @param {number} seq
539
+ # * @param {*} result
540
+ # */
541
+ # function deliverResult(name, seq, result) {
542
+ # window[name]['callbacks'].get(seq).resolve(result);
543
+ # window[name]['callbacks'].delete(seq);
544
+ # }
545
+
546
+ # /**
547
+ # * @param {string} name
548
+ # * @param {number} seq
549
+ # * @param {string} message
550
+ # * @param {string} stack
551
+ # */
552
+ # function deliverError(name, seq, message, stack) {
553
+ # const error = new Error(message);
554
+ # error.stack = stack;
555
+ # window[name]['callbacks'].get(seq).reject(error);
556
+ # window[name]['callbacks'].delete(seq);
557
+ # }
558
+
559
+ # /**
560
+ # * @param {string} name
561
+ # * @param {number} seq
562
+ # * @param {*} value
563
+ # */
564
+ # function deliverErrorValue(name, seq, value) {
565
+ # window[name]['callbacks'].get(seq).reject(value);
566
+ # window[name]['callbacks'].delete(seq);
567
+ # }
568
+ # }
569
+
570
+ # /**
571
+ # * @param {string} type
572
+ # * @param {!Array<!Puppeteer.JSHandle>} args
573
+ # * @param {Protocol.Runtime.StackTrace=} stackTrace
574
+ # */
575
+ # _addConsoleMessage(type, args, stackTrace) {
576
+ # if (!this.listenerCount(Events.Page.Console)) {
577
+ # args.forEach(arg => arg.dispose());
578
+ # return;
579
+ # }
580
+ # const textTokens = [];
581
+ # for (const arg of args) {
582
+ # const remoteObject = arg._remoteObject;
583
+ # if (remoteObject.objectId)
584
+ # textTokens.push(arg.toString());
585
+ # else
586
+ # textTokens.push(helper.valueFromRemoteObject(remoteObject));
587
+ # }
588
+ # const location = stackTrace && stackTrace.callFrames.length ? {
589
+ # url: stackTrace.callFrames[0].url,
590
+ # lineNumber: stackTrace.callFrames[0].lineNumber,
591
+ # columnNumber: stackTrace.callFrames[0].columnNumber,
592
+ # } : {};
593
+ # const message = new ConsoleMessage(type, textTokens.join(' '), args, location);
594
+ # this.emit(Events.Page.Console, message);
595
+ # }
596
+
597
+ # _onDialog(event) {
598
+ # let dialogType = null;
599
+ # if (event.type === 'alert')
600
+ # dialogType = Dialog.Type.Alert;
601
+ # else if (event.type === 'confirm')
602
+ # dialogType = Dialog.Type.Confirm;
603
+ # else if (event.type === 'prompt')
604
+ # dialogType = Dialog.Type.Prompt;
605
+ # else if (event.type === 'beforeunload')
606
+ # dialogType = Dialog.Type.BeforeUnload;
607
+ # assert(dialogType, 'Unknown javascript dialog type: ' + event.type);
608
+ # const dialog = new Dialog(this._client, dialogType, event.message, event.defaultPrompt);
609
+ # this.emit(Events.Page.Dialog, dialog);
610
+ # }
611
+
612
+ # @return [String]
613
+ def url
614
+ main_frame.url
615
+ end
616
+
617
+ # @return [String]
618
+ def content
619
+ main_frame.content
620
+ end
621
+
622
+ # @param {string} html
623
+ # @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
624
+ def set_content(html, timeout: nil, wait_until: nil)
625
+ main_frame.set_content(html, timeout: timeout, wait_until: wait_until)
626
+ end
627
+
628
+ # @param {string} html
629
+ def content=(html)
630
+ main_frame.set_content(html)
631
+ end
632
+
633
+ # @param url [String]
634
+ # @param rederer [String]
635
+ # @param timeout [number|nil]
636
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
637
+ def goto(url, referer: nil, timeout: nil, wait_until: nil)
638
+ main_frame.goto(url, referer: referer, timeout: timeout, wait_until: wait_until)
639
+ end
640
+
641
+ # @param timeout [number|nil]
642
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
643
+ # @return [Puppeteer::Response]
644
+ def reload(timeout: nil, wait_until: nil)
645
+ await_all(
646
+ async_wait_for_navigation(timeout: timeout, wait_until: wait_until),
647
+ @client.async_send_message('Page.reload'),
648
+ ).first
649
+ end
650
+
651
+ # @param timeout [number|nil]
652
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
653
+ private def wait_for_navigation(timeout: nil, wait_until: nil)
654
+ main_frame.wait_for_navigation(timeout: timeout, wait_until: wait_until)
655
+ end
656
+
657
+ # @param timeout [number|nil]
658
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
659
+ # @return [Future]
660
+ async def async_wait_for_navigation(timeout: nil, wait_until: nil)
661
+ wait_for_navigation(timeout: timeout, wait_until: wait_until)
662
+ end
663
+
664
+ # /**
665
+ # * @param {(string|Function)} urlOrPredicate
666
+ # * @param {!{timeout?: number}=} options
667
+ # * @return {!Promise<!Puppeteer.Request>}
668
+ # */
669
+ # async waitForRequest(urlOrPredicate, options = {}) {
670
+ # const {
671
+ # timeout = this._timeoutSettings.timeout(),
672
+ # } = options;
673
+ # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Request, request => {
674
+ # if (helper.isString(urlOrPredicate))
675
+ # return (urlOrPredicate === request.url());
676
+ # if (typeof urlOrPredicate === 'function')
677
+ # return !!(urlOrPredicate(request));
678
+ # return false;
679
+ # }, timeout, this._sessionClosePromise());
680
+ # }
681
+
682
+ # /**
683
+ # * @param {(string|Function)} urlOrPredicate
684
+ # * @param {!{timeout?: number}=} options
685
+ # * @return {!Promise<!Puppeteer.Response>}
686
+ # */
687
+ # async waitForResponse(urlOrPredicate, options = {}) {
688
+ # const {
689
+ # timeout = this._timeoutSettings.timeout(),
690
+ # } = options;
691
+ # return helper.waitForEvent(this._frameManager.networkManager(), Events.NetworkManager.Response, response => {
692
+ # if (helper.isString(urlOrPredicate))
693
+ # return (urlOrPredicate === response.url());
694
+ # if (typeof urlOrPredicate === 'function')
695
+ # return !!(urlOrPredicate(response));
696
+ # return false;
697
+ # }, timeout, this._sessionClosePromise());
698
+ # }
699
+
700
+ # @param timeout [number|nil]
701
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
702
+ def go_back(timeout: nil, wait_until: nil)
703
+ go(-1, timeout: timeout, wait_until: wait_until)
704
+ end
705
+
706
+ # @param timeout [number|nil]
707
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
708
+ def go_forward(timeout: nil, wait_until: nil)
709
+ go(+1, timeout: timeout, wait_until: wait_until)
710
+ end
711
+
712
+ private def go(delta, timeout: nil, wait_until: nil)
713
+ history = @client.send_message('Page.getNavigationHistory')
714
+ entries = history['entries']
715
+ index = history['currentIndex'] + delta
716
+ if_present(entries[index]) do |entry|
717
+ await_all(
718
+ async_wait_for_navigation(timeout: timeout, wait_until: wait_until),
719
+ @client.async_send_message('Page.navigateToHistoryEntry', entryId: entry['id']),
720
+ )
721
+ end
722
+ end
723
+
724
+ # @param device [Device]
725
+ def emulate(device)
726
+ self.viewport = device.viewport
727
+ self.user_agent = device.user_agent
728
+ end
729
+
730
+ # @param {boolean} enabled
731
+ def javascript_enabled=(enabled)
732
+ return if @javascript_enabled == enabled
733
+ @javascript_enabled = enabled
734
+ @client.send_message('Emulation.setScriptExecutionDisabled', value: !enabled)
735
+ end
736
+
737
+ # /**
738
+ # * @param {boolean} enabled
739
+ # */
740
+ # async setBypassCSP(enabled) {
741
+ # await this._client.send('Page.setBypassCSP', { enabled });
742
+ # }
743
+
744
+ # /**
745
+ # * @param {?string} type
746
+ # */
747
+ # async emulateMediaType(type) {
748
+ # assert(type === 'screen' || type === 'print' || type === null, 'Unsupported media type: ' + type);
749
+ # await this._client.send('Emulation.setEmulatedMedia', {media: type || ''});
750
+ # }
751
+
752
+ # /**
753
+ # * @param {?Array<MediaFeature>} features
754
+ # */
755
+ # async emulateMediaFeatures(features) {
756
+ # if (features === null)
757
+ # await this._client.send('Emulation.setEmulatedMedia', {features: null});
758
+ # if (Array.isArray(features)) {
759
+ # features.every(mediaFeature => {
760
+ # const name = mediaFeature.name;
761
+ # assert(/^prefers-(?:color-scheme|reduced-motion)$/.test(name), 'Unsupported media feature: ' + name);
762
+ # return true;
763
+ # });
764
+ # await this._client.send('Emulation.setEmulatedMedia', {features: features});
765
+ # }
766
+ # }
767
+
768
+ # @param timezone_id [String?]
769
+ def emulate_timezone(timezone_id)
770
+ @client.send_message('Emulation.setTimezoneOverride', timezoneId: timezoneId || '')
771
+ rescue => err
772
+ if err.message.include?('Invalid timezone')
773
+ raise ArgumentError.new("Invalid timezone ID: #{timezone_id}")
774
+ else
775
+ raise err
776
+ end
777
+ end
778
+
779
+ # @param viewport [Viewport]
780
+ def viewport=(viewport)
781
+ needs_reload = @emulation_manager.emulate_viewport(viewport)
782
+ @viewport = viewport
783
+ reload if needs_reload
784
+ end
785
+
786
+ attr_reader :viewport
787
+
788
+ # @param {Function|string} pageFunction
789
+ # @param {!Array<*>} args
790
+ # @return {!Promise<*>}
791
+ def evaluate(page_function, *args)
792
+ main_frame.evaluate(page_function, *args)
793
+ end
794
+
795
+ # /**
796
+ # * @param {Function|string} pageFunction
797
+ # * @param {!Array<*>} args
798
+ # */
799
+ # async evaluateOnNewDocument(pageFunction, ...args) {
800
+ # const source = helper.evaluationString(pageFunction, ...args);
801
+ # await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source });
802
+ # }
803
+
804
+ # @param {boolean} enabled
805
+ def cache_enabled=(enabled)
806
+ @frame_manager.network_manager.cache_enabled = enabled
807
+ end
808
+
809
+ # @return {!Promise<string>}
810
+ def title
811
+ @title
812
+ end
813
+
814
+ # /**
815
+ # * @param {!ScreenshotOptions=} options
816
+ # * @return {!Promise<!Buffer|!String>}
817
+ # */
818
+ def screenshot(options = {})
819
+ screenshot_options = ScreenshotOptions.new(options)
820
+
821
+ # @screenshot_task_queue.post_task(-> { screenshot_task(screenshot_options.type, screenshot_options) })
822
+ screenshot_task(screenshot_options.type, screenshot_options)
823
+ end
824
+
825
+ # @param {"png"|"jpeg"} format
826
+ # @param {!ScreenshotOptions=} options
827
+ # @return {!Promise<!Buffer|!String>}
828
+ private def screenshot_task(format, screenshot_options)
829
+ @client.send_message('Target.activateTarget', targetId: @target.target_id)
830
+
831
+ clip = if_present(screenshot_options.clip) do |rect|
832
+ x = rect[:x].round
833
+ y = rect[:y].round
834
+ { x: x, y: y, width: rect[:width] + rect[:x] - x, height: rect[:height] + rect[:y] - y, scale: 1 }
835
+ end
836
+
837
+ if screenshot_options.full_page?
838
+ metrics = @client.send_message('Page.getLayoutMetrics')
839
+ width = metrics['contentSize']['width'].ceil
840
+ height = metrics['contentSize']['height'].ceil
841
+
842
+ # Overwrite clip for full page at all times.
843
+ clip = { x: 0, y: 0, width: width, height: height, scale: 1 }
844
+
845
+ screen_orientation =
846
+ if @viewport.landscape?
847
+ { angle: 90, type: 'landscapePrimary' }
848
+ else
849
+ { angle: 0, type: 'portraitPrimary' }
850
+ end
851
+ @client.send_message('Emulation.setDeviceMetricsOverride',
852
+ mobile: @viewport.mobile?,
853
+ width: width,
854
+ height: height,
855
+ deviceScaleFactor: @viewport.device_scale_factor,
856
+ screenOrientation: screen_orientation)
857
+ end
858
+
859
+ should_set_default_background = screenshot_options.omit_background? && format == 'png'
860
+ if should_set_default_background
861
+ @client.send_message('Emulation.setDefaultBackgroundColorOverride', color: { r: 0, g: 0, b: 0, a: 0 })
862
+ end
863
+ screenshot_params = {
864
+ format: format,
865
+ quality: screenshot_options.quality,
866
+ clip: clip,
867
+ }.compact
868
+ result = @client.send_message('Page.captureScreenshot', screenshot_params)
869
+ if should_set_default_background
870
+ @client.send_message('Emulation.setDefaultBackgroundColorOverride')
871
+ end
872
+
873
+ if screenshot_options.full_page? && @viewport
874
+ self.viewport = @viewport
875
+ end
876
+
877
+ buffer =
878
+ if screenshot_options.encoding == 'base64'
879
+ result['data']
880
+ else
881
+ Base64.decode64(result['data'])
882
+ end
883
+
884
+ if screenshot_options.path
885
+ File.binwrite(screenshot_options.path, buffer)
886
+ end
887
+
888
+ buffer
889
+ end
890
+
891
+ # /**
892
+ # * @param {!PDFOptions=} options
893
+ # * @return {!Promise<!Buffer>}
894
+ # */
895
+ # async pdf(options = {}) {
896
+ # const {
897
+ # scale = 1,
898
+ # displayHeaderFooter = false,
899
+ # headerTemplate = '',
900
+ # footerTemplate = '',
901
+ # printBackground = false,
902
+ # landscape = false,
903
+ # pageRanges = '',
904
+ # preferCSSPageSize = false,
905
+ # margin = {},
906
+ # path = null
907
+ # } = options;
908
+
909
+ # let paperWidth = 8.5;
910
+ # let paperHeight = 11;
911
+ # if (options.format) {
912
+ # const format = Page.PaperFormats[options.format.toLowerCase()];
913
+ # assert(format, 'Unknown paper format: ' + options.format);
914
+ # paperWidth = format.width;
915
+ # paperHeight = format.height;
916
+ # } else {
917
+ # paperWidth = convertPrintParameterToInches(options.width) || paperWidth;
918
+ # paperHeight = convertPrintParameterToInches(options.height) || paperHeight;
919
+ # }
920
+
921
+ # const marginTop = convertPrintParameterToInches(margin.top) || 0;
922
+ # const marginLeft = convertPrintParameterToInches(margin.left) || 0;
923
+ # const marginBottom = convertPrintParameterToInches(margin.bottom) || 0;
924
+ # const marginRight = convertPrintParameterToInches(margin.right) || 0;
925
+
926
+ # const result = await this._client.send('Page.printToPDF', {
927
+ # transferMode: 'ReturnAsStream',
928
+ # landscape,
929
+ # displayHeaderFooter,
930
+ # headerTemplate,
931
+ # footerTemplate,
932
+ # printBackground,
933
+ # scale,
934
+ # paperWidth,
935
+ # paperHeight,
936
+ # marginTop,
937
+ # marginBottom,
938
+ # marginLeft,
939
+ # marginRight,
940
+ # pageRanges,
941
+ # preferCSSPageSize
942
+ # });
943
+ # return await helper.readProtocolStream(this._client, result.stream, path);
944
+ # }
945
+
946
+ # @param {!{runBeforeUnload: (boolean|undefined)}=} options
947
+ def close
948
+ # assert(!!this._client._connection, 'Protocol error: Connection closed. Most likely the page has been closed.');
949
+ # const runBeforeUnload = !!options.runBeforeUnload;
950
+ # if (runBeforeUnload) {
951
+ # await this._client.send('Page.close');
952
+ # } else {
953
+ # await this._client._connection.send('Target.closeTarget', { targetId: this._target._targetId });
954
+ # await this._target._isClosedPromise;
955
+ # }
956
+ end
957
+
958
+ # @return [boolean]
959
+ def closed?
960
+ @closed
961
+ end
962
+
963
+ attr_reader :mouse
964
+
965
+ # @param selector [String]
966
+ # @param delay [Number]
967
+ # @param button [String] "left"|"right"|"middle"
968
+ # @param click_count [Number]
969
+ def click(selector, delay: nil, button: nil, click_count: nil)
970
+ main_frame.click(selector, delay: delay, button: button, click_count: click_count)
971
+ end
972
+
973
+ # @param selector [String]
974
+ # @param delay [Number]
975
+ # @param button [String] "left"|"right"|"middle"
976
+ # @param click_count [Number]
977
+ # @return [Future]
978
+ async def async_click(selector, delay: nil, button: nil, click_count: nil)
979
+ click(selector, delay: delay, button: button, click_count: click_count)
980
+ end
981
+
982
+ # @param {string} selector
983
+ def focus(selector)
984
+ main_frame.focus(selector)
985
+ end
986
+
987
+ # @param {string} selector
988
+ def hover(selector)
989
+ main_frame.hover(selector)
990
+ end
991
+
992
+ # @param {string} selector
993
+ # @param {!Array<string>} values
994
+ # @return {!Promise<!Array<string>>}
995
+ def select(selector, *values)
996
+ main_frame.select(selector, *values)
997
+ end
998
+
999
+ # @param selector [String]
1000
+ def tap(selector)
1001
+ main_frame.tap(selector)
1002
+ end
1003
+
1004
+ # @param selector [String]
1005
+ async def async_tap(selector)
1006
+ tap(selector)
1007
+ end
1008
+
1009
+ # @param selector [String]
1010
+ # @param text [String]
1011
+ # @param delay [Number]
1012
+ def type_text(selector, text, delay: nil)
1013
+ main_frame.type_text(selector, text, delay: delay)
1014
+ end
1015
+
1016
+ # /**
1017
+ # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
1018
+ # * @param {!{visible?: boolean, hidden?: boolean, timeout?: number, polling?: string|number}=} options
1019
+ # * @param {!Array<*>} args
1020
+ # * @return {!Promise<!Puppeteer.JSHandle>}
1021
+ # */
1022
+ # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
1023
+ # return this.mainFrame().waitFor(selectorOrFunctionOrTimeout, options, ...args);
1024
+ # }
1025
+
1026
+ # @param selector [String]
1027
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1028
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1029
+ # @param timeout [Integer]
1030
+ def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
1031
+ main_frame.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
1032
+ end
1033
+
1034
+ # @param selector [String]
1035
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1036
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1037
+ # @param timeout [Integer]
1038
+ async def async_wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
1039
+ wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
1040
+ end
1041
+
1042
+ # @param xpath [String]
1043
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1044
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1045
+ # @param timeout [Integer]
1046
+ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
1047
+ main_frame.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
1048
+ end
1049
+
1050
+ # @param xpath [String]
1051
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
1052
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
1053
+ # @param timeout [Integer]
1054
+ async def async_wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
1055
+ wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
1056
+ end
1057
+
1058
+ # @param {Function|string} pageFunction
1059
+ # @param {!{polling?: string|number, timeout?: number}=} options
1060
+ # @param {!Array<*>} args
1061
+ # @return {!Promise<!Puppeteer.JSHandle>}
1062
+ def wait_for_function(page_function, options = {}, *args)
1063
+ main_frame.wait_for_function(page_function, options, *args)
1064
+ end
1065
+ end