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,286 @@
1
+ class Puppeteer::Frame
2
+ # @param {!FrameManager} frameManager
3
+ # @param {!Puppeteer.CDPSession} client
4
+ # @param {?Frame} parentFrame
5
+ # @param {string} frameId
6
+ def initialize(frame_manager, client, parent_frame, frame_id)
7
+ @frame_manager = frame_manager
8
+ @client = client
9
+ @parent_frame = parent_frame
10
+ @id = frame_id
11
+ @detached = false
12
+
13
+ @loader_id = ''
14
+ @lifecycle_events = Set.new
15
+ @main_world = Puppeteer::DOMWorld.new(frame_manager, self, frame_manager.timeout_settings)
16
+ @secondary_world = Puppeteer::DOMWorld.new(frame_manager, self, frame_manager.timeout_settings)
17
+ @child_frames = Set.new
18
+ if parent_frame
19
+ parent_frame._child_frames << self
20
+ end
21
+ end
22
+
23
+ attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :secondary_world
24
+
25
+ # @param url [String]
26
+ # @param rederer [String]
27
+ # @param timeout [number|nil]
28
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
29
+ # @return [Puppeteer::Response]
30
+ def goto(url, referer: nil, timeout: nil, wait_until: nil)
31
+ @frame_manager.navigate_frame(self, url, referer: referer, timeout: timeout, wait_until: wait_until)
32
+ end
33
+
34
+ # @param timeout [number|nil]
35
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
36
+ def wait_for_navigation(timeout: nil, wait_until: nil)
37
+ @frame_manager.wait_for_frame_navigation(self, timeout: timeout, wait_until: wait_until)
38
+ end
39
+
40
+ def execution_context
41
+ @main_world.execution_context
42
+ end
43
+
44
+ # @param {Function|string} pageFunction
45
+ # @return {!Promise<!Puppeteer.JSHandle>}
46
+ def evaluate_handle(page_function, *args)
47
+ @main_world.evaluate_handle(page_function, *args)
48
+ end
49
+
50
+ # @param {Function|string} pageFunction
51
+ # @param {!Array<*>} args
52
+ def evaluate(page_function, *args)
53
+ @main_world.evaluate(page_function, *args)
54
+ end
55
+
56
+ # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
57
+ # @param {string} selector
58
+ # @return {!Promise<?Puppeteer.ElementHandle>}
59
+ def S(selector)
60
+ @main_world.S(selector)
61
+ end
62
+
63
+
64
+ # `$x()` in JavaScript. $ is not allowed to use as a method name in Ruby.
65
+ # @param {string} expression
66
+ # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
67
+ def Sx(expression)
68
+ @main_world.Sx(expression)
69
+ end
70
+
71
+
72
+ # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
73
+ # @param {string} selector
74
+ # @param {Function|string} pageFunction
75
+ # @param {!Array<*>} args
76
+ # @return {!Promise<(!Object|undefined)>}
77
+ def Seval(selector, page_function, *args)
78
+ @main_world.Seval(selector, page_function, *args)
79
+ end
80
+
81
+ # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
82
+ # @param {string} selector
83
+ # @param {Function|string} pageFunction
84
+ # @param {!Array<*>} args
85
+ # @return {!Promise<(!Object|undefined)>}
86
+ def SSeval(selector, page_function, *args)
87
+ @main_world.SSeval(selector, page_function, *args)
88
+ end
89
+
90
+ # `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
91
+ # @param {string} selector
92
+ # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
93
+ def SS(selector)
94
+ @main_world.SS(selector)
95
+ end
96
+
97
+ def content
98
+ @secondary_world.content
99
+ end
100
+
101
+ # @param {string} html
102
+ # @param {!{timeout?: number, waitUntil?: string|!Array<string>}=} options
103
+ def set_content(html, timeout: nil, wait_until: nil)
104
+ @secondary_world.set_content(html, timeout: timeout, wait_until: wait_until)
105
+ end
106
+
107
+ # @return [String]
108
+ def name
109
+ @name || ''
110
+ end
111
+
112
+ # @return [String]
113
+ def url
114
+ @url
115
+ end
116
+
117
+ # @return [Frame?]
118
+ def parent_frame
119
+ @parent_frame
120
+ end
121
+
122
+ protected def _child_frames
123
+ @child_frames
124
+ end
125
+
126
+ def child_frames
127
+ @child_frames.dup
128
+ end
129
+
130
+ def detached?
131
+ @detached
132
+ end
133
+
134
+ # @param style_tag [Puppeteer::Page::ScriptTag]
135
+ # @return {!Promise<!ElementHandle>}
136
+ def add_script_tag(script_tag)
137
+ @main_world.add_script_tag(script_tag)
138
+ end
139
+
140
+ # @param style_tag [Puppeteer::Page::StyleTag]
141
+ # @return {!Promise<!ElementHandle>}
142
+ def add_style_tag(style_tag)
143
+ @main_world.add_style_tag(style_tag)
144
+ end
145
+
146
+ # @param selector [String]
147
+ # @param delay [Number]
148
+ # @param button [String] "left"|"right"|"middle"
149
+ # @param click_count [Number]
150
+ def click(selector, delay: nil, button: nil, click_count: nil)
151
+ @secondary_world.click(selector, delay: delay, button: button, click_count: click_count)
152
+ end
153
+
154
+ # @param {string} selector
155
+ def focus(selector)
156
+ @secondary_world.focus(selector)
157
+ end
158
+
159
+ # @param {string} selector
160
+ def hover(selector)
161
+ @secondary_world.hover(selector)
162
+ end
163
+
164
+ # @param {string} selector
165
+ # @param {!Array<string>} values
166
+ # @return {!Promise<!Array<string>>}
167
+ def select(selector, *values)
168
+ @secondary_world.select(selector, *values)
169
+ end
170
+
171
+ # @param {string} selector
172
+ def tap(selector)
173
+ @secondary_world.tap(selector)
174
+ end
175
+
176
+ # @param selector [String]
177
+ # @param text [String]
178
+ # @param delay [Number]
179
+ def type_text(selector, text, delay: nil)
180
+ @main_world.type_text(selector, text, delay: delay)
181
+ end
182
+
183
+ # /**
184
+ # * @param {(string|number|Function)} selectorOrFunctionOrTimeout
185
+ # * @param {!Object=} options
186
+ # * @param {!Array<*>} args
187
+ # * @return {!Promise<?Puppeteer.JSHandle>}
188
+ # */
189
+ # waitFor(selectorOrFunctionOrTimeout, options = {}, ...args) {
190
+ # const xPathPattern = '//';
191
+
192
+ # if (helper.isString(selectorOrFunctionOrTimeout)) {
193
+ # const string = /** @type {string} */ (selectorOrFunctionOrTimeout);
194
+ # if (string.startsWith(xPathPattern))
195
+ # return this.waitForXPath(string, options);
196
+ # return this.waitForSelector(string, options);
197
+ # }
198
+ # if (helper.isNumber(selectorOrFunctionOrTimeout))
199
+ # return new Promise(fulfill => setTimeout(fulfill, /** @type {number} */ (selectorOrFunctionOrTimeout)));
200
+ # if (typeof selectorOrFunctionOrTimeout === 'function')
201
+ # return this.waitForFunction(selectorOrFunctionOrTimeout, options, ...args);
202
+ # return Promise.reject(new Error('Unsupported target type: ' + (typeof selectorOrFunctionOrTimeout)));
203
+ # }
204
+
205
+ # @param selector [String]
206
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
207
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
208
+ # @param timeout [Integer]
209
+ def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
210
+ handle = @secondary_world.wait_for_selector(selector, visible: visible, hidden: hidden, timeout: timeout)
211
+ if !handle
212
+ return nil
213
+ end
214
+ main_execution_context = @main_world.execution_context
215
+ result = main_execution_context.adopt_element_handle(handle)
216
+ handle.dispose
217
+ result
218
+ end
219
+
220
+ # @param xpath [String]
221
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
222
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
223
+ # @param timeout [Integer]
224
+ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
225
+ handle = @secondary_world.wait_for_xpath(xpath, visible: visible, hidden: hidden, timeout: timeout)
226
+ if !handle
227
+ return nil
228
+ end
229
+ main_execution_context = @main_world.execution_context
230
+ result = main_execution_context.adopt_element_handle(handle)
231
+ handle.dispose
232
+ result
233
+ end
234
+
235
+ # @param {Function|string} pageFunction
236
+ # @param {!{polling?: string|number, timeout?: number}=} options
237
+ # @param {!Array<*>} args
238
+ # @return {!Promise<!Puppeteer.JSHandle>}
239
+ def wait_for_function(page_function, options = {}, *args)
240
+ @main_world.wait_for_function(page_function, options, *args)
241
+ end
242
+
243
+ def title
244
+ @secondary_world.title
245
+ end
246
+
247
+ # @param frame_payload [Hash]
248
+ def navigated(frame_payload)
249
+ @name = frame_payload['name']
250
+ # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
251
+ @navigation_url = frame_payload['url']
252
+ @url = frame_payload['url']
253
+
254
+ # Ensure loaderId updated.
255
+ # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
256
+ @loader_id = frame_payload['loaderId']
257
+ end
258
+
259
+ # @param url [String]
260
+ def navigated_within_document(url)
261
+ @url = url
262
+ end
263
+
264
+ def handle_lifecycle_event(loader_id, name)
265
+ if name == 'init'
266
+ @loader_id = loader_id
267
+ @lifecycle_events.clear
268
+ end
269
+ @lifecycle_events << name
270
+ end
271
+
272
+ def handle_loading_stopped
273
+ @lifecycle_events << 'DOMContentLoaded'
274
+ @lifecycle_events << 'load'
275
+ end
276
+
277
+ def detach
278
+ @detached = true
279
+ @main_world.detach
280
+ @secondary_world.detach
281
+ if @parent_frame
282
+ @parent_frame._child_frames.delete(self)
283
+ end
284
+ @parent_frame = nil
285
+ end
286
+ end
@@ -0,0 +1,378 @@
1
+ require 'timeout'
2
+
3
+ class Puppeteer::FrameManager
4
+ include Puppeteer::DebugPrint
5
+ include Puppeteer::IfPresent
6
+ include Puppeteer::EventCallbackable
7
+ using Puppeteer::AsyncAwaitBehavior
8
+
9
+ UTILITY_WORLD_NAME = '__puppeteer_utility_world__'
10
+
11
+ # @param {!Puppeteer.CDPSession} client
12
+ # @param {!Puppeteer.Page} page
13
+ # @param {boolean} ignoreHTTPSErrors
14
+ # @param {!Puppeteer.TimeoutSettings} timeoutSettings
15
+ def initialize(client, page, ignore_https_errors, timeout_settings)
16
+ @client = client
17
+ @page = page
18
+ @network_manager = Puppeteer::NetworkManager.new(client, ignore_https_errors, self)
19
+ @timeout_settings = timeout_settings
20
+
21
+ # @type {!Map<string, !Frame>}
22
+ @frames = {}
23
+
24
+ # @type {!Map<number, !ExecutionContext>}
25
+ @context_id_to_context = {}
26
+ @context_id_created = {}
27
+
28
+ # @type {!Set<string>}
29
+ @isolated_worlds = Set.new
30
+
31
+ @client.on_event 'Page.frameAttached' do |event|
32
+ handle_frame_attached(event['frameId'], event['parentFrameId'])
33
+ end
34
+ @client.on_event 'Page.frameNavigated' do |event|
35
+ handle_frame_navigated(event['frame'])
36
+ end
37
+ @client.on_event 'Page.navigatedWithinDocument' do |event|
38
+ handle_frame_navigated_within_document(event['frameId'], event['url'])
39
+ end
40
+ @client.on_event 'Page.frameDetached' do |event|
41
+ handle_frame_detached(event['frameId'])
42
+ end
43
+ @client.on_event 'Page.frameStoppedLoading' do |event|
44
+ handle_frame_stopped_loading(event['frameId'])
45
+ end
46
+ @client.on_event 'Runtime.executionContextCreated' do |event|
47
+ handle_execution_context_created(event['context'])
48
+ end
49
+ @client.on_event 'Runtime.executionContextDestroyed' do |event|
50
+ handle_execution_context_destroyed(event['executionContextId'])
51
+ end
52
+ @client.on_event 'Runtime.executionContextsCleared' do |event|
53
+ handle_execution_contexts_cleared
54
+ end
55
+ @client.on_event 'Page.lifecycleEvent' do |event|
56
+ handle_lifecycle_event(event)
57
+ end
58
+ end
59
+
60
+ attr_reader :client, :timeout_settings
61
+
62
+ private def init
63
+ results = await_all(
64
+ @client.async_send_message('Page.enable'),
65
+ @client.async_send_message('Page.getFrameTree'),
66
+ )
67
+ frame_tree = results.last['frameTree']
68
+ handle_frame_tree(frame_tree)
69
+ await_all(
70
+ @client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
71
+ @client.async_send_message('Runtime.enable'),
72
+ )
73
+ ensure_isolated_world(UTILITY_WORLD_NAME)
74
+ @network_manager.init
75
+ end
76
+
77
+ async def async_init
78
+ init
79
+ end
80
+
81
+ attr_reader :network_manager
82
+
83
+ class NavigationError < StandardError; end
84
+
85
+ # @param frame [Puppeteer::Frame]
86
+ # @param url [String]
87
+ # @param {!{referer?: string, timeout?: number, waitUntil?: string|!Array<string>}=} options
88
+ # @return [Puppeteer::Response]
89
+ def navigate_frame(frame, url, referer: nil, timeout: nil, wait_until: nil)
90
+ assert_no_legacy_navigation_options(wait_until: wait_until)
91
+
92
+ navigate_params = {
93
+ url: url,
94
+ referer: referer || @network_manager.extra_http_headers['referer'],
95
+ frameId: frame.id,
96
+ }.compact
97
+ option_wait_until = wait_until || ['load']
98
+ option_timeout = timeout || @timeout_settings.navigation_timeout
99
+
100
+ watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
101
+ ensure_new_document_navigation = false
102
+
103
+ begin
104
+ navigate = future do
105
+ result = @client.send_message('Page.navigate', navigate_params)
106
+ loader_id = result['loaderId']
107
+ ensure_new_document_navigation = !!loader_id
108
+ if result['errorText']
109
+ raise NavigationError.new("#{result['errorText']} at #{url}")
110
+ end
111
+ end
112
+ await_any(
113
+ navigate,
114
+ watcher.timeout_or_termination_promise,
115
+ )
116
+
117
+ document_navigation_promise =
118
+ if ensure_new_document_navigation
119
+ watcher.new_document_navigation_promise
120
+ else
121
+ watcher.same_document_navigation_promise
122
+ end
123
+ await_any(
124
+ document_navigation_promise,
125
+ watcher.timeout_or_termination_promise,
126
+ )
127
+ ensure
128
+ watcher.dispose
129
+ end
130
+
131
+ watcher.navigation_response
132
+ end
133
+
134
+ # @param timeout [number|nil]
135
+ # @param wait_until [string|nil] 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
136
+ # @return [Puppeteer::Response]
137
+ def wait_for_frame_navigation(frame, timeout: nil, wait_until: nil)
138
+ assert_no_legacy_navigation_options(wait_until: wait_until)
139
+
140
+ option_wait_until = wait_until || ['load']
141
+ option_timeout = timeout || @timeout_settings.navigation_timeout
142
+ watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
143
+ begin
144
+ await_any(
145
+ watcher.timeout_or_termination_promise,
146
+ watcher.same_document_navigation_promise,
147
+ watcher.new_document_navigation_promise,
148
+ )
149
+ ensure
150
+ watcher.dispose
151
+ end
152
+
153
+ watcher.navigation_response
154
+ end
155
+
156
+ # @param event [Hash]
157
+ def handle_lifecycle_event(event)
158
+ frame = @frames[event['frameId']]
159
+ return if !frame
160
+ frame.handle_lifecycle_event(event['loaderId'], event['name'])
161
+ emit_event 'Events.FrameManager.LifecycleEvent', frame
162
+ end
163
+
164
+ # @param {string} frameId
165
+ def handle_frame_stopped_loading(frame_id)
166
+ frame = @frames[frame_id]
167
+ return if !frame
168
+ frame.handle_loading_stopped
169
+ emit_event 'Events.FrameManager.LifecycleEvent', frame
170
+ end
171
+
172
+ # @param frame_tree [Hash]
173
+ def handle_frame_tree(frame_tree)
174
+ if frame_tree['frame']['parentId']
175
+ handle_frame_attached(frame_tree['frame']['id'], frame_tree['frame']['parentId'])
176
+ end
177
+ handle_frame_navigated(frame_tree['frame'])
178
+ return if !frame_tree['childFrames']
179
+
180
+ frame_tree['childFrames'].each do |child|
181
+ handle_frame_tree(child)
182
+ end
183
+ end
184
+
185
+ # @return {!Puppeteer.Page}
186
+ def page
187
+ @page
188
+ end
189
+
190
+ # @return {!Frame}
191
+ def main_frame
192
+ @main_frame
193
+ end
194
+
195
+ # @return {!Array<!Frame>}
196
+ def frames
197
+ @frames.values
198
+ end
199
+
200
+ # @param {!string} frameId
201
+ # @return {?Frame}
202
+ def frame(frame_id)
203
+ @frames[frame_id]
204
+ end
205
+
206
+ # @param {string} frameId
207
+ # @param {?string} parentFrameId
208
+ def handle_frame_attached(frame_id, parent_frame_id)
209
+ return if @frames.has_key?(frame_id)
210
+ if !parent_frame_id
211
+ raise ArgymentError.new('parent_frame_id must not be nil')
212
+ end
213
+ parent_frame = @frames[parent_frame_id]
214
+ frame = Puppeteer::Frame.new(self, @client, parent_frame, frame_id)
215
+ @frames[frame_id] = frame
216
+
217
+ emit_event 'Events.FrameManager.FrameAttached', frame
218
+ end
219
+
220
+ # @param frame_payload [Hash]
221
+ def handle_frame_navigated(frame_payload)
222
+ is_main_frame = !frame_payload['parentId']
223
+ frame =
224
+ if is_main_frame
225
+ @main_frame
226
+ else
227
+ @frames[frame_payload['id']]
228
+ end
229
+
230
+ if !is_main_frame && !frame
231
+ raise ArgumentError.new('We either navigate top level or have old version of the navigated frame')
232
+ end
233
+
234
+ # Detach all child frames first.
235
+ if frame
236
+ frame.child_frames.each do |child|
237
+ remove_frame_recursively(child)
238
+ end
239
+ end
240
+
241
+ # Update or create main frame.
242
+ if is_main_frame
243
+ if frame
244
+ # Update frame id to retain frame identity on cross-process navigation.
245
+ @frames.delete(frame.id)
246
+ frame.id = frame_payload['id']
247
+ else
248
+ # Initial main frame navigation.
249
+ frame = Puppeteer::Frame.new(self, @client, nil, frame_payload['id'])
250
+ end
251
+ @frames[frame_payload['id']] = frame
252
+ @main_frame = frame
253
+ end
254
+
255
+ # Update frame payload.
256
+ frame.navigated(frame_payload)
257
+
258
+ emit_event 'Events.FrameManager.FrameNavigated', frame
259
+ end
260
+
261
+ # @param name [String]
262
+ def ensure_isolated_world(name)
263
+ return if @isolated_worlds.include?(name)
264
+ @isolated_worlds << name
265
+
266
+ @client.send_message('Page.addScriptToEvaluateOnNewDocument',
267
+ source: "//# sourceURL=#{Puppeteer::ExecutionContext::EVALUATION_SCRIPT_URL}",
268
+ worldName: name,
269
+ )
270
+ create_isolated_worlds_promises = frames.map do |frame|
271
+ @client.async_send_message('Page.createIsolatedWorld',
272
+ frameId: frame.id,
273
+ grantUniveralAccess: true,
274
+ worldName: name,
275
+ )
276
+ end
277
+ await_all(*create_isolated_worlds_promises)
278
+ end
279
+
280
+ # @param frame_id [String]
281
+ # @param url [String]
282
+ def handle_frame_navigated_within_document(frame_id, url)
283
+ frame = @frames[frame_id]
284
+ return if !frame
285
+ frame.navigated_within_document(url)
286
+ emit_event 'Events.FrameManager.FrameNavigatedWithinDocument', frame
287
+ emit_event 'Events.FrameManager.FrameNavigated', frame
288
+ end
289
+
290
+ # @param frame_id [String]
291
+ def handle_frame_detached(frame_id)
292
+ frame = @frames[frame_id]
293
+ if frame
294
+ remove_frame_recursively(frame)
295
+ end
296
+ end
297
+
298
+ # @param context_payload [Hash]
299
+ def handle_execution_context_created(context_payload)
300
+ frame = if_present(context_payload.dig('auxData', 'frameId')) { |frame_id| @frames[frame_id] }
301
+
302
+ world = nil
303
+ if frame
304
+ if context_payload.dig('auxData', 'isDefault')
305
+ world = frame.main_world
306
+ elsif context_payload['name'] == UTILITY_WORLD_NAME && !frame.secondary_world.has_context?
307
+ # In case of multiple sessions to the same target, there's a race between
308
+ # connections so we might end up creating multiple isolated worlds.
309
+ # We can use either.
310
+ world = frame.secondary_world
311
+ end
312
+ end
313
+
314
+ if context_payload.dig('auxData', 'type') == 'isolated'
315
+ @isolated_worlds << context_payload['name']
316
+ end
317
+
318
+ context = Puppeteer::ExecutionContext.new(@client, context_payload, world)
319
+ if world
320
+ world.context = context
321
+ end
322
+ @context_id_to_context[context_payload['id']] = context
323
+ @context_id_created[context_payload['id']] = Time.now
324
+ end
325
+
326
+ # @param {number} executionContextId
327
+ def handle_execution_context_destroyed(execution_context_id)
328
+ context = @context_id_to_context[execution_context_id]
329
+ return if !context
330
+ @context_id_to_context.delete(execution_context_id)
331
+ @context_id_created.delete(execution_context_id)
332
+ if context.world
333
+ context.world.delete_context(execution_context_id)
334
+ end
335
+ end
336
+
337
+ def handle_execution_contexts_cleared
338
+ # executionContextsCleared is often notified after executionContextCreated.
339
+ # D, [2020-04-06T01:47:03.101227 #13823] DEBUG -- : RECV << {"method"=>"Runtime.executionContextCreated", "params"=>{"context"=>{"id"=>5, "origin"=>"https://github.com", "name"=>"", "auxData"=>{"isDefault"=>true, "type"=>"default", "frameId"=>"71C347B70848B89DDDEFAA8AB5B0BC92"}}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
340
+ # D, [2020-04-06T01:47:03.101439 #13823] DEBUG -- : RECV << {"method"=>"Page.frameNavigated", "params"=>{"frame"=>{"id"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "loaderId"=>"80338225D035AC96BAE8F6D4E81C7D51", "url"=>"https://github.com/search?q=puppeteer", "securityOrigin"=>"https://github.com", "mimeType"=>"text/html"}}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
341
+ # D, [2020-04-06T01:47:03.101325 #13823] DEBUG -- : RECV << {"method"=>"Target.targetInfoChanged", "params"=>{"targetInfo"=>{"targetId"=>"71C347B70848B89DDDEFAA8AB5B0BC92", "type"=>"page", "title"=>"https://github.com/search?q=puppeteer", "url"=>"https://github.com/search?q=puppeteer", "attached"=>true, "browserContextId"=>"AF37BC660284CE1552B4ECB147BE9305"}}}
342
+ # D, [2020-04-06T01:47:03.101269 #13823] DEBUG -- : RECV << {"method"=>"Runtime.executionContextsCleared", "params"=>{}, "sessionId"=>"53F088EED260C28001D26A019F95D9E3"}
343
+ # it unexpectedly clears the created execution context.
344
+ # To avoid the problem, just skip recent created ids.
345
+ now = Time.now
346
+ context_ids_to_skip = @context_id_created.select { |k, v| now - v < 1 }.keys
347
+ @context_id_to_context.reject { |k, v| context_ids_to_skip.include?(k) }.each do |execution_context_id, context|
348
+ if context.world
349
+ context.world.delete_context(execution_context_id)
350
+ end
351
+ end
352
+ @context_id_to_context.select! { |k, v| context_ids_to_skip.include?(k) }
353
+ end
354
+
355
+ def execution_context_by_id(context_id)
356
+ context = @context_id_to_context[context_id]
357
+ if !context
358
+ raise "INTERNAL ERROR: missing context with id = #{context_id}"
359
+ end
360
+ context
361
+ end
362
+
363
+ # @param {!Frame} frame
364
+ private def remove_frame_recursively(frame)
365
+ frame.child_frames.each do |child|
366
+ remove_frame_recursively(child)
367
+ end
368
+ frame.detach
369
+ @frames.delete(frame.id)
370
+ emit_event 'Events.FrameManager.FrameDetached', frame
371
+ end
372
+
373
+ private def assert_no_legacy_navigation_options(wait_until:)
374
+ if wait_until == 'networkidle'
375
+ raise ArgumentError.new('ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead')
376
+ end
377
+ end
378
+ end