ferrum 0.17.1 → 0.18.0
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 +4 -4
- data/LICENSE +1 -1
- data/README.md +9 -1369
- data/lib/ferrum/accessibility/ax_node.rb +108 -0
- data/lib/ferrum/accessibility.rb +106 -0
- data/lib/ferrum/browser/binary.rb +41 -0
- data/lib/ferrum/browser/command.rb +20 -0
- data/lib/ferrum/browser/options/base.rb +69 -2
- data/lib/ferrum/browser/options/chrome.rb +88 -26
- data/lib/ferrum/browser/options/firefox.rb +32 -0
- data/lib/ferrum/browser/options.rb +43 -3
- data/lib/ferrum/browser/process.rb +56 -4
- data/lib/ferrum/browser/xvfb.rb +24 -0
- data/lib/ferrum/browser.rb +35 -9
- data/lib/ferrum/client/subscriber.rb +58 -0
- data/lib/ferrum/client/web_socket.rb +63 -12
- data/lib/ferrum/client.rb +206 -14
- data/lib/ferrum/context.rb +99 -4
- data/lib/ferrum/contexts.rb +136 -22
- data/lib/ferrum/cookies/cookie.rb +7 -1
- data/lib/ferrum/cookies.rb +5 -0
- data/lib/ferrum/dialog.rb +18 -2
- data/lib/ferrum/downloads.rb +52 -0
- data/lib/ferrum/errors.rb +62 -6
- data/lib/ferrum/frame/dom.rb +17 -0
- data/lib/ferrum/frame/runtime.rb +49 -8
- data/lib/ferrum/frame.rb +58 -1
- data/lib/ferrum/headers.rb +6 -0
- data/lib/ferrum/interceptable.rb +62 -0
- data/lib/ferrum/keyboard.rb +25 -0
- data/lib/ferrum/mouse.rb +6 -0
- data/lib/ferrum/network/auth_request.rb +81 -2
- data/lib/ferrum/network/error.rb +15 -0
- data/lib/ferrum/network/exchange.rb +10 -0
- data/lib/ferrum/network/intercepted_request.rb +94 -2
- data/lib/ferrum/network/request.rb +1 -1
- data/lib/ferrum/network/response.rb +16 -1
- data/lib/ferrum/network.rb +122 -10
- data/lib/ferrum/node.rb +305 -15
- data/lib/ferrum/page/animation.rb +7 -2
- data/lib/ferrum/page/frames.rb +69 -7
- data/lib/ferrum/page/screencast.rb +5 -0
- data/lib/ferrum/page/screenshot.rb +52 -20
- data/lib/ferrum/page/stream.rb +56 -0
- data/lib/ferrum/page/tracing.rb +6 -0
- data/lib/ferrum/page.rb +141 -46
- data/lib/ferrum/proxy.rb +52 -2
- data/lib/ferrum/rgba.rb +10 -0
- data/lib/ferrum/target.rb +124 -1
- data/lib/ferrum/utils/attempt.rb +20 -0
- data/lib/ferrum/utils/elapsed_time.rb +38 -0
- data/lib/ferrum/utils/event.rb +14 -0
- data/lib/ferrum/utils/platform.rb +22 -1
- data/lib/ferrum/utils/thread.rb +12 -0
- data/lib/ferrum/version.rb +1 -1
- data/lib/ferrum/worker.rb +125 -0
- data/lib/ferrum.rb +7 -0
- metadata +8 -21
data/lib/ferrum/client.rb
CHANGED
|
@@ -7,9 +7,29 @@ require "ferrum/client/web_socket"
|
|
|
7
7
|
require "ferrum/utils/thread"
|
|
8
8
|
|
|
9
9
|
module Ferrum
|
|
10
|
+
#
|
|
11
|
+
# A thin wrapper around {Client} that scopes commands and events to a
|
|
12
|
+
# single CDP session (a `sessionId` obtained via `Target.attachToTarget`).
|
|
13
|
+
# Targets connect through one of these rather than the top-level {Client}
|
|
14
|
+
# directly, so their commands/events don't leak into other sessions.
|
|
15
|
+
# Method calls not defined here are forwarded to the underlying {Client}.
|
|
16
|
+
#
|
|
10
17
|
class SessionClient
|
|
11
18
|
attr_reader :client, :session_id
|
|
12
19
|
|
|
20
|
+
#
|
|
21
|
+
# Builds the internal event key used to route an event to a specific
|
|
22
|
+
# session, joining the CDP event name with a session id (or leaving it
|
|
23
|
+
# session-less when `session_id` is `nil`).
|
|
24
|
+
#
|
|
25
|
+
# @param [String] event
|
|
26
|
+
# The CDP event name, e.g. `"Page.loadEventFired"`.
|
|
27
|
+
#
|
|
28
|
+
# @param [String, nil] session_id
|
|
29
|
+
# The target session id, or `nil` for the browser-wide session.
|
|
30
|
+
#
|
|
31
|
+
# @return [String]
|
|
32
|
+
#
|
|
13
33
|
def self.event_name(event, session_id)
|
|
14
34
|
[event, session_id].compact.join("_")
|
|
15
35
|
end
|
|
@@ -19,31 +39,93 @@ module Ferrum
|
|
|
19
39
|
@session_id = session_id
|
|
20
40
|
end
|
|
21
41
|
|
|
22
|
-
|
|
42
|
+
#
|
|
43
|
+
# Sends a CDP command scoped to this session.
|
|
44
|
+
#
|
|
45
|
+
# @param [String] method
|
|
46
|
+
# The CDP method name, e.g. `"Page.navigate"`.
|
|
47
|
+
#
|
|
48
|
+
# @param [Boolean] async
|
|
49
|
+
# Whether to send the command without waiting for a response.
|
|
50
|
+
#
|
|
51
|
+
# @param [Hash] params
|
|
52
|
+
# The command's parameters.
|
|
53
|
+
#
|
|
54
|
+
# @param [Numeric, nil] timeout
|
|
55
|
+
# How long to wait for this command's response, overriding
|
|
56
|
+
# {Browser::Options#protocol_timeout}. See {Client#send_message}.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean, Hash]
|
|
59
|
+
# `true` when sent asynchronously, otherwise the command's result.
|
|
60
|
+
#
|
|
61
|
+
def command(method, async: false, timeout: nil, **params)
|
|
23
62
|
message = build_message(method, params)
|
|
24
|
-
@client.send_message(message, async: async)
|
|
63
|
+
@client.send_message(message, async: async, timeout: timeout)
|
|
25
64
|
end
|
|
26
65
|
|
|
27
|
-
|
|
28
|
-
|
|
66
|
+
#
|
|
67
|
+
# Subscribes to a CDP event scoped to this session.
|
|
68
|
+
#
|
|
69
|
+
# @param [String] event
|
|
70
|
+
# The CDP event name.
|
|
71
|
+
#
|
|
72
|
+
# @return [Integer]
|
|
73
|
+
# The subscription id, used to unsubscribe via {#off}.
|
|
74
|
+
#
|
|
75
|
+
def on(event, &)
|
|
76
|
+
@client.on(event_name(event), &)
|
|
29
77
|
end
|
|
30
78
|
|
|
79
|
+
#
|
|
80
|
+
# Unsubscribes from a CDP event scoped to this session.
|
|
81
|
+
#
|
|
82
|
+
# @param [String] event
|
|
83
|
+
# The CDP event name.
|
|
84
|
+
#
|
|
85
|
+
# @param [Integer] id
|
|
86
|
+
# The subscription id returned by {#on}.
|
|
87
|
+
#
|
|
88
|
+
# @return [void]
|
|
89
|
+
#
|
|
31
90
|
def off(event, id)
|
|
32
91
|
@client.off(event_name(event), id)
|
|
33
92
|
end
|
|
34
93
|
|
|
94
|
+
# Whether there's at least one callback registered for the event, scoped
|
|
95
|
+
# to this session.
|
|
96
|
+
#
|
|
97
|
+
# @param [String] event
|
|
98
|
+
# The CDP event name.
|
|
99
|
+
#
|
|
100
|
+
# @return [Boolean]
|
|
35
101
|
def subscribed?(event)
|
|
36
102
|
@client.subscribed?(event_name(event))
|
|
37
103
|
end
|
|
38
104
|
|
|
105
|
+
# Supports {#method_missing} delegation by reporting the underlying
|
|
106
|
+
# {Client}'s methods as responded to.
|
|
107
|
+
#
|
|
108
|
+
# @return [Boolean]
|
|
39
109
|
def respond_to_missing?(name, include_private)
|
|
40
110
|
@client.respond_to?(name, include_private)
|
|
41
111
|
end
|
|
42
112
|
|
|
43
|
-
|
|
44
|
-
|
|
113
|
+
#
|
|
114
|
+
# Delegates any method not defined on `SessionClient` to the underlying
|
|
115
|
+
# {Client}, e.g. `subscribed?`.
|
|
116
|
+
#
|
|
117
|
+
# @return [untyped]
|
|
118
|
+
#
|
|
119
|
+
def method_missing(name, ...)
|
|
120
|
+
@client.send(name, ...)
|
|
45
121
|
end
|
|
46
122
|
|
|
123
|
+
#
|
|
124
|
+
# Removes all event callbacks registered for this session from the
|
|
125
|
+
# underlying client's subscriber.
|
|
126
|
+
#
|
|
127
|
+
# @return [void]
|
|
128
|
+
#
|
|
47
129
|
def close
|
|
48
130
|
@client.subscriber.clear(session_id: session_id)
|
|
49
131
|
end
|
|
@@ -59,14 +141,22 @@ module Ferrum
|
|
|
59
141
|
end
|
|
60
142
|
end
|
|
61
143
|
|
|
144
|
+
#
|
|
145
|
+
# The low-level CDP client. Owns the {WebSocket} connection to the browser,
|
|
146
|
+
# assigns command ids, matches responses back to their pending commands and
|
|
147
|
+
# dispatches incoming events to the {Subscriber}. {SessionClient} builds on
|
|
148
|
+
# top of it to scope commands/events to a particular target's session.
|
|
149
|
+
#
|
|
62
150
|
class Client
|
|
63
151
|
extend Forwardable
|
|
64
|
-
|
|
152
|
+
|
|
153
|
+
delegate %i[protocol_timeout protocol_timeout=] => :options
|
|
65
154
|
|
|
66
155
|
attr_reader :ws_url, :options, :subscriber
|
|
67
156
|
|
|
68
157
|
def initialize(ws_url, options)
|
|
69
158
|
@command_id = 0
|
|
159
|
+
@command_id_mutex = Mutex.new
|
|
70
160
|
@ws_url = ws_url
|
|
71
161
|
@options = options
|
|
72
162
|
@pendings = Concurrent::Hash.new
|
|
@@ -76,12 +166,55 @@ module Ferrum
|
|
|
76
166
|
start
|
|
77
167
|
end
|
|
78
168
|
|
|
79
|
-
|
|
169
|
+
#
|
|
170
|
+
# Sends a CDP command to the browser-wide session.
|
|
171
|
+
#
|
|
172
|
+
# @param [String] method
|
|
173
|
+
# The CDP method name, e.g. `"Target.createTarget"`.
|
|
174
|
+
#
|
|
175
|
+
# @param [Boolean] async
|
|
176
|
+
# Whether to send the command without waiting for a response.
|
|
177
|
+
#
|
|
178
|
+
# @param [Hash] params
|
|
179
|
+
# The command's parameters.
|
|
180
|
+
#
|
|
181
|
+
# @param [Numeric, nil] timeout
|
|
182
|
+
# How long to wait for this command's response, overriding
|
|
183
|
+
# {Browser::Options#protocol_timeout}. See {#send_message}.
|
|
184
|
+
#
|
|
185
|
+
# @return [Boolean, Hash]
|
|
186
|
+
# `true` when sent asynchronously, otherwise the command's result.
|
|
187
|
+
#
|
|
188
|
+
def command(method, async: false, timeout: nil, **params)
|
|
80
189
|
message = build_message(method, params)
|
|
81
|
-
send_message(message, async: async)
|
|
190
|
+
send_message(message, async: async, timeout: timeout)
|
|
82
191
|
end
|
|
83
192
|
|
|
84
|
-
|
|
193
|
+
#
|
|
194
|
+
# Sends a raw CDP message over the websocket. Synchronous calls block
|
|
195
|
+
# until a matching response arrives, or `timeout` elapses, defaulting to
|
|
196
|
+
# `protocol_timeout` (delegated to {Browser::Options#protocol_timeout}).
|
|
197
|
+
# That default is the transport-level budget for internal CDP bookkeeping
|
|
198
|
+
# (e.g. `Target.createTarget`). {Page#command} overrides
|
|
199
|
+
# this back to `timeout`, or a caller-supplied budget (e.g. `#pdf`/
|
|
200
|
+
# `#screenshot`'s own `timeout:` argument), for the user-facing commands
|
|
201
|
+
# it issues -- some of which (e.g. `Page.navigate`, `Page.printToPDF`)
|
|
202
|
+
# rely on their own response latency to detect a stuck operation.
|
|
203
|
+
#
|
|
204
|
+
# @param [Hash] message
|
|
205
|
+
# The message to send, must include an `:id` key.
|
|
206
|
+
#
|
|
207
|
+
# @param [Boolean] async
|
|
208
|
+
# Whether to return immediately instead of waiting for a response.
|
|
209
|
+
#
|
|
210
|
+
# @param [Numeric, nil] timeout
|
|
211
|
+
# How long to wait for the response. Defaults to `protocol_timeout`.
|
|
212
|
+
#
|
|
213
|
+
# @return [Boolean, Hash]
|
|
214
|
+
# `true` when sent asynchronously, otherwise the parsed `"result"`
|
|
215
|
+
# from the response.
|
|
216
|
+
#
|
|
217
|
+
def send_message(message, async:, timeout: nil)
|
|
85
218
|
if async
|
|
86
219
|
@ws.send_message(message)
|
|
87
220
|
true
|
|
@@ -89,7 +222,7 @@ module Ferrum
|
|
|
89
222
|
pending = Concurrent::IVar.new
|
|
90
223
|
@pendings[message[:id]] = pending
|
|
91
224
|
@ws.send_message(message)
|
|
92
|
-
data = pending.value!(timeout)
|
|
225
|
+
data = pending.value!(timeout || protocol_timeout)
|
|
93
226
|
@pendings.delete(message[:id])
|
|
94
227
|
|
|
95
228
|
raise DeadBrowserError if data.nil? && @ws.messages.closed?
|
|
@@ -101,22 +234,63 @@ module Ferrum
|
|
|
101
234
|
end
|
|
102
235
|
end
|
|
103
236
|
|
|
104
|
-
|
|
105
|
-
|
|
237
|
+
#
|
|
238
|
+
# Subscribes to a CDP event.
|
|
239
|
+
#
|
|
240
|
+
# @param [String] event
|
|
241
|
+
# The CDP event name.
|
|
242
|
+
#
|
|
243
|
+
# @return [Integer]
|
|
244
|
+
# The subscription id, used to unsubscribe via {#off}.
|
|
245
|
+
#
|
|
246
|
+
def on(event, &)
|
|
247
|
+
@subscriber.on(event, &)
|
|
106
248
|
end
|
|
107
249
|
|
|
250
|
+
#
|
|
251
|
+
# Unsubscribes from a CDP event.
|
|
252
|
+
#
|
|
253
|
+
# @param [String] event
|
|
254
|
+
# The CDP event name.
|
|
255
|
+
#
|
|
256
|
+
# @param [Integer] id
|
|
257
|
+
# The subscription id returned by {#on}.
|
|
258
|
+
#
|
|
259
|
+
# @return [void]
|
|
260
|
+
#
|
|
108
261
|
def off(event, id)
|
|
109
262
|
@subscriber.off(event, id)
|
|
110
263
|
end
|
|
111
264
|
|
|
265
|
+
# Whether there's at least one callback registered for the event.
|
|
266
|
+
#
|
|
267
|
+
# @param [String] event
|
|
268
|
+
# The CDP event name.
|
|
269
|
+
#
|
|
270
|
+
# @return [Boolean]
|
|
112
271
|
def subscribed?(event)
|
|
113
272
|
@subscriber.subscribed?(event)
|
|
114
273
|
end
|
|
115
274
|
|
|
275
|
+
#
|
|
276
|
+
# Builds a client scoped to a given CDP session, e.g. a browsing
|
|
277
|
+
# context created via `Target.attachToTarget`.
|
|
278
|
+
#
|
|
279
|
+
# @param [String] session_id
|
|
280
|
+
# The CDP session id to scope commands and events to.
|
|
281
|
+
#
|
|
282
|
+
# @return [SessionClient]
|
|
283
|
+
#
|
|
116
284
|
def session(session_id)
|
|
117
285
|
SessionClient.new(self, session_id)
|
|
118
286
|
end
|
|
119
287
|
|
|
288
|
+
#
|
|
289
|
+
# Closes the underlying websocket, drops pending commands and stops
|
|
290
|
+
# the message-processing thread and subscriber.
|
|
291
|
+
#
|
|
292
|
+
# @return [void]
|
|
293
|
+
#
|
|
120
294
|
def close
|
|
121
295
|
@ws.close
|
|
122
296
|
# Give a thread some time to handle a tail of messages
|
|
@@ -125,6 +299,11 @@ module Ferrum
|
|
|
125
299
|
@subscriber.close
|
|
126
300
|
end
|
|
127
301
|
|
|
302
|
+
#
|
|
303
|
+
# Custom inspection that exposes internal state useful for debugging.
|
|
304
|
+
#
|
|
305
|
+
# @return [String]
|
|
306
|
+
#
|
|
128
307
|
def inspect
|
|
129
308
|
"#<#{self.class} " \
|
|
130
309
|
"@command_id=#{@command_id.inspect} " \
|
|
@@ -132,6 +311,17 @@ module Ferrum
|
|
|
132
311
|
"@ws=#{@ws.inspect}>"
|
|
133
312
|
end
|
|
134
313
|
|
|
314
|
+
#
|
|
315
|
+
# Builds a CDP message hash with a fresh, thread-safe command id.
|
|
316
|
+
#
|
|
317
|
+
# @param [String] method
|
|
318
|
+
# The CDP method name.
|
|
319
|
+
#
|
|
320
|
+
# @param [Hash] params
|
|
321
|
+
# The command's parameters.
|
|
322
|
+
#
|
|
323
|
+
# @return [Hash]
|
|
324
|
+
#
|
|
135
325
|
def build_message(method, params)
|
|
136
326
|
{ method: method, params: params }.merge(id: next_command_id)
|
|
137
327
|
end
|
|
@@ -153,8 +343,10 @@ module Ferrum
|
|
|
153
343
|
end
|
|
154
344
|
end
|
|
155
345
|
|
|
346
|
+
# Locked so two concurrent commands never share an id and read each
|
|
347
|
+
# other's responses from @pendings.
|
|
156
348
|
def next_command_id
|
|
157
|
-
@command_id += 1
|
|
349
|
+
@command_id_mutex.synchronize { @command_id += 1 }
|
|
158
350
|
end
|
|
159
351
|
|
|
160
352
|
def raise_browser_error(error)
|
data/lib/ferrum/context.rb
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
require "ferrum/target"
|
|
4
4
|
|
|
5
5
|
module Ferrum
|
|
6
|
+
#
|
|
7
|
+
# Represents a browser context, i.e. an isolated browsing profile (similar
|
|
8
|
+
# to an incognito window) with its own cookies, cache and storage. Keeps
|
|
9
|
+
# track of the {Target}s that belong to it and connects to them as {Page}s
|
|
10
|
+
# or {Worker}s. Managed by {Contexts}, which owns the browser's collection
|
|
11
|
+
# of contexts and routes CDP target events to the right one.
|
|
12
|
+
#
|
|
6
13
|
class Context
|
|
7
14
|
POSITION = %i[first last].freeze
|
|
8
15
|
|
|
@@ -16,16 +23,43 @@ module Ferrum
|
|
|
16
23
|
@pendings = Concurrent::Map.new
|
|
17
24
|
end
|
|
18
25
|
|
|
26
|
+
# The context's first known target, creating one via
|
|
27
|
+
# `Target.createTarget` if none has attached yet.
|
|
28
|
+
#
|
|
29
|
+
# @return [Target]
|
|
19
30
|
def default_target
|
|
20
31
|
@default_target ||= create_target
|
|
21
32
|
end
|
|
22
33
|
|
|
34
|
+
# Connects to and returns the {#default_target}'s page.
|
|
35
|
+
#
|
|
36
|
+
# @return [Page]
|
|
23
37
|
def page
|
|
24
38
|
default_target.page
|
|
25
39
|
end
|
|
26
40
|
|
|
41
|
+
# All page targets in this context, connected to as {Page}s.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<Page>]
|
|
27
44
|
def pages
|
|
28
|
-
@targets.values.
|
|
45
|
+
@targets.values.select(&:page?).map(&:page)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Dedicated and shared workers spawned by any page in this context.
|
|
49
|
+
#
|
|
50
|
+
# @return [Array<Worker>]
|
|
51
|
+
def workers
|
|
52
|
+
@targets.values.select { |t| t.worker? || t.shared_worker? }.map(&:worker)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Service worker targets registered in this context. Unlike {#workers},
|
|
56
|
+
# these are plain {Target}s and are not connected to. Attaching to a
|
|
57
|
+
# service worker's session keeps it alive indefinitely, so we only do
|
|
58
|
+
# that on demand, via `target.worker`.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Target>]
|
|
61
|
+
def service_workers
|
|
62
|
+
@targets.values.select(&:service_worker?)
|
|
29
63
|
end
|
|
30
64
|
|
|
31
65
|
# When we call `page` method on target it triggers ruby to connect to given
|
|
@@ -40,74 +74,135 @@ module Ferrum
|
|
|
40
74
|
windows.map(&:page)
|
|
41
75
|
end
|
|
42
76
|
|
|
77
|
+
# Creates a new target in this context and eagerly connects to it as a
|
|
78
|
+
# {Page}, forwarding `options` to {Target#build_page}.
|
|
79
|
+
#
|
|
80
|
+
# @param [Hash] options
|
|
81
|
+
#
|
|
82
|
+
# @return [Page]
|
|
43
83
|
def create_page(**options)
|
|
44
84
|
target = create_target
|
|
45
85
|
target.page = target.build_page(**options)
|
|
46
86
|
end
|
|
47
87
|
|
|
88
|
+
# Creates a new target in this context via `Target.createTarget` and
|
|
89
|
+
# blocks until it's been registered (see {#add_target}).
|
|
90
|
+
#
|
|
91
|
+
# @return [Target]
|
|
92
|
+
#
|
|
93
|
+
# @raise [NoSuchTargetError]
|
|
48
94
|
def create_target
|
|
49
95
|
target_id = @client.command("Target.createTarget", browserContextId: @id, url: "about:blank")["targetId"]
|
|
50
96
|
|
|
51
97
|
new_pending = Concurrent::IVar.new
|
|
52
98
|
pending = @pendings.put_if_absent(target_id, new_pending) || new_pending
|
|
53
|
-
resolved = pending.value(@client.
|
|
99
|
+
resolved = pending.value(@client.protocol_timeout)
|
|
54
100
|
raise NoSuchTargetError unless resolved
|
|
55
101
|
|
|
56
102
|
@pendings.delete(target_id)
|
|
57
103
|
@targets[target_id]
|
|
58
104
|
end
|
|
59
105
|
|
|
106
|
+
# Registers a target discovered via a CDP `Target.*` event, or updates
|
|
107
|
+
# the session id on one already known. Called by {Contexts} as targets
|
|
108
|
+
# are created/attached.
|
|
109
|
+
#
|
|
110
|
+
# @param [Hash] params
|
|
111
|
+
# The target's `targetInfo`.
|
|
112
|
+
#
|
|
113
|
+
# @param [String, nil] session_id
|
|
114
|
+
#
|
|
115
|
+
# @return [Target]
|
|
60
116
|
def add_target(params:, session_id: nil)
|
|
61
117
|
new_target = Target.new(@client, session_id, params)
|
|
62
118
|
# `put_if_absent` returns nil if added a new value or existing if there was one already
|
|
63
119
|
target = @targets.put_if_absent(new_target.id, new_target) || new_target
|
|
120
|
+
# on first iteration session_id may be nil, then if session is present here we must set it to the target
|
|
121
|
+
target.session_id = session_id if session_id && target.session_id.nil?
|
|
64
122
|
@default_target ||= target
|
|
65
123
|
|
|
66
124
|
new_pending = Concurrent::IVar.new
|
|
67
125
|
pending = @pendings.put_if_absent(target.id, new_pending) || new_pending
|
|
68
126
|
pending.try_set(true)
|
|
69
|
-
|
|
127
|
+
target
|
|
70
128
|
end
|
|
71
129
|
|
|
130
|
+
# Updates a known target's params, e.g. on `Target.targetInfoChanged`.
|
|
131
|
+
#
|
|
132
|
+
# @param [String] target_id
|
|
133
|
+
#
|
|
134
|
+
# @param [Hash] params
|
|
135
|
+
#
|
|
136
|
+
# @return [void]
|
|
72
137
|
def update_target(target_id, params)
|
|
73
138
|
@targets[target_id]&.update(params)
|
|
74
139
|
end
|
|
75
140
|
|
|
141
|
+
# Removes a target, e.g. on `Target.targetDestroyed`/`targetCrashed`.
|
|
142
|
+
#
|
|
143
|
+
# @param [String] target_id
|
|
144
|
+
#
|
|
145
|
+
# @return [Target, nil]
|
|
76
146
|
def delete_target(target_id)
|
|
77
147
|
@targets.delete(target_id)
|
|
78
148
|
end
|
|
79
149
|
|
|
150
|
+
# Manually attaches to a target, e.g. a service worker discovered via
|
|
151
|
+
# {#service_workers}. Once attached, `target.worker`/`target.page`
|
|
152
|
+
# returns a connected {Worker}/{Page} for it.
|
|
153
|
+
#
|
|
154
|
+
# Note: attaching to a service worker's session prevents Chrome from
|
|
155
|
+
# ever terminating it while the connection is open.
|
|
80
156
|
def attach_target(target_id)
|
|
81
157
|
target = @targets[target_id]
|
|
82
158
|
raise NoSuchTargetError unless target
|
|
83
159
|
|
|
160
|
+
@contexts.manually_attached(target_id)
|
|
84
161
|
session = @client.command("Target.attachToTarget", targetId: target_id, flatten: true)
|
|
85
162
|
target.session_id = session["sessionId"]
|
|
86
163
|
true
|
|
87
164
|
end
|
|
88
165
|
|
|
166
|
+
# Returns the first target for which the block returns truthy.
|
|
167
|
+
#
|
|
168
|
+
# @return [Target, nil]
|
|
89
169
|
def find_target
|
|
90
170
|
@targets.each_value { |t| return t if yield(t) }
|
|
91
171
|
|
|
92
172
|
nil
|
|
93
173
|
end
|
|
94
174
|
|
|
175
|
+
# Closes the WebSocket connection of every connected target, without
|
|
176
|
+
# disposing the targets themselves.
|
|
177
|
+
#
|
|
178
|
+
# @return [void]
|
|
95
179
|
def close_targets_connection
|
|
96
180
|
@targets.each_value do |target|
|
|
97
181
|
next unless target.connected?
|
|
98
182
|
|
|
99
|
-
target.
|
|
183
|
+
target.close_connection
|
|
100
184
|
end
|
|
101
185
|
end
|
|
102
186
|
|
|
187
|
+
# Disposes this browser context and all of its targets.
|
|
188
|
+
#
|
|
189
|
+
# @return [Boolean]
|
|
103
190
|
def dispose
|
|
104
191
|
@contexts.dispose(@id)
|
|
105
192
|
end
|
|
106
193
|
|
|
194
|
+
# Whether a target with the given id is known in this context.
|
|
195
|
+
#
|
|
196
|
+
# @param [String] target_id
|
|
197
|
+
#
|
|
198
|
+
# @return [Boolean]
|
|
107
199
|
def target?(target_id)
|
|
108
200
|
!!@targets[target_id]
|
|
109
201
|
end
|
|
110
202
|
|
|
203
|
+
# Debug representation of the context, including its known targets.
|
|
204
|
+
#
|
|
205
|
+
# @return [String]
|
|
111
206
|
def inspect
|
|
112
207
|
%(#<#{self.class} @id=#{@id.inspect} @targets=#{@targets.inspect} @default_target=#{@default_target.inspect}>)
|
|
113
208
|
end
|