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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +71 -0
- data/.github/stale.yml +16 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +302 -0
- data/.travis.yml +7 -0
- data/Dockerfile +6 -0
- data/Gemfile +6 -0
- data/README.md +54 -0
- data/Rakefile +1 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/docker-compose.yml +15 -0
- data/docs/Puppeteer.html +2020 -0
- data/docs/Puppeteer/AsyncAwaitBehavior.html +105 -0
- data/docs/Puppeteer/Browser.html +2148 -0
- data/docs/Puppeteer/BrowserContext.html +809 -0
- data/docs/Puppeteer/BrowserFetcher.html +214 -0
- data/docs/Puppeteer/BrowserRunner.html +914 -0
- data/docs/Puppeteer/BrowserRunner/BrowserProcess.html +477 -0
- data/docs/Puppeteer/CDPSession.html +813 -0
- data/docs/Puppeteer/CDPSession/Error.html +124 -0
- data/docs/Puppeteer/ConcurrentRubyUtils.html +430 -0
- data/docs/Puppeteer/Connection.html +960 -0
- data/docs/Puppeteer/Connection/MessageCallback.html +434 -0
- data/docs/Puppeteer/Connection/ProtocolError.html +216 -0
- data/docs/Puppeteer/Connection/RequestDebugPrinter.html +217 -0
- data/docs/Puppeteer/Connection/ResponseDebugPrinter.html +244 -0
- data/docs/Puppeteer/ConsoleMessage.html +565 -0
- data/docs/Puppeteer/ConsoleMessage/Location.html +433 -0
- data/docs/Puppeteer/DOMWorld.html +2219 -0
- data/docs/Puppeteer/DOMWorld/DetachedError.html +124 -0
- data/docs/Puppeteer/DOMWorld/DocumentEvaluationError.html +124 -0
- data/docs/Puppeteer/DebugPrint.html +233 -0
- data/docs/Puppeteer/Device.html +470 -0
- data/docs/Puppeteer/Devices.html +139 -0
- data/docs/Puppeteer/ElementHandle.html +2542 -0
- data/docs/Puppeteer/ElementHandle/ElementNotFoundError.html +206 -0
- data/docs/Puppeteer/ElementHandle/ElementNotVisibleError.html +206 -0
- data/docs/Puppeteer/ElementHandle/Point.html +492 -0
- data/docs/Puppeteer/ElementHandle/ScrollIntoViewError.html +124 -0
- data/docs/Puppeteer/EmulationManager.html +454 -0
- data/docs/Puppeteer/EventCallbackable.html +433 -0
- data/docs/Puppeteer/EventCallbackable/EventListeners.html +435 -0
- data/docs/Puppeteer/ExecutionContext.html +998 -0
- data/docs/Puppeteer/ExecutionContext/EvaluationError.html +124 -0
- data/docs/Puppeteer/ExecutionContext/JavaScriptExpression.html +357 -0
- data/docs/Puppeteer/ExecutionContext/JavaScriptFunction.html +389 -0
- data/docs/Puppeteer/FileChooser.html +455 -0
- data/docs/Puppeteer/Frame.html +3677 -0
- data/docs/Puppeteer/FrameManager.html +2410 -0
- data/docs/Puppeteer/FrameManager/NavigationError.html +124 -0
- data/docs/Puppeteer/IfPresent.html +222 -0
- data/docs/Puppeteer/JSHandle.html +1352 -0
- data/docs/Puppeteer/Keyboard.html +1557 -0
- data/docs/Puppeteer/Keyboard/KeyDefinition.html +831 -0
- data/docs/Puppeteer/Keyboard/KeyDescription.html +603 -0
- data/docs/Puppeteer/Launcher.html +237 -0
- data/docs/Puppeteer/Launcher/Base.html +385 -0
- data/docs/Puppeteer/Launcher/Base/ExecutablePathNotFound.html +124 -0
- data/docs/Puppeteer/Launcher/BrowserOptions.html +441 -0
- data/docs/Puppeteer/Launcher/Chrome.html +669 -0
- data/docs/Puppeteer/Launcher/Chrome/DefaultArgs.html +382 -0
- data/docs/Puppeteer/Launcher/ChromeArgOptions.html +531 -0
- data/docs/Puppeteer/Launcher/LaunchOptions.html +893 -0
- data/docs/Puppeteer/LifecycleWatcher.html +834 -0
- data/docs/Puppeteer/LifecycleWatcher/ExpectedLifecycle.html +363 -0
- data/docs/Puppeteer/LifecycleWatcher/FrameDetachedError.html +206 -0
- data/docs/Puppeteer/LifecycleWatcher/TerminatedError.html +124 -0
- data/docs/Puppeteer/Mouse.html +1105 -0
- data/docs/Puppeteer/Mouse/Button.html +136 -0
- data/docs/Puppeteer/NetworkManager.html +901 -0
- data/docs/Puppeteer/NetworkManager/Credentials.html +385 -0
- data/docs/Puppeteer/Page.html +5970 -0
- data/docs/Puppeteer/Page/FileChooserTimeoutError.html +206 -0
- data/docs/Puppeteer/Page/ScreenshotOptions.html +845 -0
- data/docs/Puppeteer/Page/ScriptTag.html +555 -0
- data/docs/Puppeteer/Page/StyleTag.html +448 -0
- data/docs/Puppeteer/Page/TargetCrashedError.html +124 -0
- data/docs/Puppeteer/RemoteObject.html +1087 -0
- data/docs/Puppeteer/Target.html +1336 -0
- data/docs/Puppeteer/Target/InitializeFailure.html +124 -0
- data/docs/Puppeteer/Target/TargetInfo.html +729 -0
- data/docs/Puppeteer/TimeoutError.html +135 -0
- data/docs/Puppeteer/TimeoutSettings.html +496 -0
- data/docs/Puppeteer/TouchScreen.html +464 -0
- data/docs/Puppeteer/Viewport.html +837 -0
- data/docs/Puppeteer/WaitTask.html +637 -0
- data/docs/Puppeteer/WaitTask/TerminatedError.html +124 -0
- data/docs/Puppeteer/WaitTask/TimeoutError.html +206 -0
- data/docs/Puppeteer/WebSocket.html +673 -0
- data/docs/Puppeteer/WebSocket/DriverImpl.html +412 -0
- data/docs/Puppeteer/WebSocketTransport.html +600 -0
- data/docs/Puppeteer/WebSocktTransportError.html +124 -0
- data/docs/_index.html +823 -0
- data/docs/class_list.html +51 -0
- data/docs/css/common.css +1 -0
- data/docs/css/full_list.css +58 -0
- data/docs/css/style.css +496 -0
- data/docs/file.README.html +123 -0
- data/docs/file_list.html +56 -0
- data/docs/frames.html +17 -0
- data/docs/index.html +123 -0
- data/docs/js/app.js +314 -0
- data/docs/js/full_list.js +216 -0
- data/docs/js/jquery.js +4 -0
- data/docs/method_list.html +4075 -0
- data/docs/top-level-namespace.html +126 -0
- data/lib/puppeteer.rb +200 -0
- data/lib/puppeteer/async_await_behavior.rb +38 -0
- data/lib/puppeteer/browser.rb +259 -0
- data/lib/puppeteer/browser_context.rb +90 -0
- data/lib/puppeteer/browser_fetcher.rb +6 -0
- data/lib/puppeteer/browser_runner.rb +161 -0
- data/lib/puppeteer/cdp_session.rb +100 -0
- data/lib/puppeteer/concurrent_ruby_utils.rb +37 -0
- data/lib/puppeteer/connection.rb +254 -0
- data/lib/puppeteer/console_message.rb +24 -0
- data/lib/puppeteer/debug_print.rb +20 -0
- data/lib/puppeteer/device.rb +12 -0
- data/lib/puppeteer/devices.rb +885 -0
- data/lib/puppeteer/dom_world.rb +484 -0
- data/lib/puppeteer/element_handle.rb +433 -0
- data/lib/puppeteer/element_handle/bounding_box.rb +12 -0
- data/lib/puppeteer/element_handle/box_model.rb +19 -0
- data/lib/puppeteer/element_handle/point.rb +26 -0
- data/lib/puppeteer/emulation_manager.rb +46 -0
- data/lib/puppeteer/errors.rb +2 -0
- data/lib/puppeteer/event_callbackable.rb +88 -0
- data/lib/puppeteer/execution_context.rb +254 -0
- data/lib/puppeteer/file_chooser.rb +29 -0
- data/lib/puppeteer/frame.rb +286 -0
- data/lib/puppeteer/frame_manager.rb +378 -0
- data/lib/puppeteer/if_present.rb +18 -0
- data/lib/puppeteer/js_handle.rb +142 -0
- data/lib/puppeteer/keyboard.rb +183 -0
- data/lib/puppeteer/keyboard/key_description.rb +19 -0
- data/lib/puppeteer/keyboard/us_keyboard_layout.rb +283 -0
- data/lib/puppeteer/launcher.rb +25 -0
- data/lib/puppeteer/launcher/base.rb +48 -0
- data/lib/puppeteer/launcher/browser_options.rb +41 -0
- data/lib/puppeteer/launcher/chrome.rb +211 -0
- data/lib/puppeteer/launcher/chrome_arg_options.rb +49 -0
- data/lib/puppeteer/launcher/launch_options.rb +68 -0
- data/lib/puppeteer/lifecycle_watcher.rb +171 -0
- data/lib/puppeteer/mouse.rb +123 -0
- data/lib/puppeteer/network_manager.rb +122 -0
- data/lib/puppeteer/page.rb +1065 -0
- data/lib/puppeteer/page/screenshot_options.rb +78 -0
- data/lib/puppeteer/remote_object.rb +143 -0
- data/lib/puppeteer/target.rb +150 -0
- data/lib/puppeteer/timeout_settings.rb +15 -0
- data/lib/puppeteer/touch_screen.rb +43 -0
- data/lib/puppeteer/version.rb +3 -0
- data/lib/puppeteer/viewport.rb +54 -0
- data/lib/puppeteer/wait_task.rb +188 -0
- data/lib/puppeteer/web_socket.rb +122 -0
- data/lib/puppeteer/web_socket_transport.rb +49 -0
- data/puppeteer-ruby.gemspec +32 -0
- 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
|