puppeteer-ruby 0.0.10

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