puppeteer-ruby 0.45.6 → 0.50.0.alpha5
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/.rubocop.yml +1 -3
- data/AGENTS.md +169 -0
- data/CLAUDE/README.md +41 -0
- data/CLAUDE/architecture.md +253 -0
- data/CLAUDE/cdp_protocol.md +230 -0
- data/CLAUDE/concurrency.md +216 -0
- data/CLAUDE/porting_puppeteer.md +575 -0
- data/CLAUDE/rbs_type_checking.md +101 -0
- data/CLAUDE/spec_migration_plans.md +1041 -0
- data/CLAUDE/testing.md +278 -0
- data/CLAUDE.md +242 -0
- data/README.md +8 -0
- data/Rakefile +7 -0
- data/Steepfile +28 -0
- data/docs/api_coverage.md +105 -56
- data/lib/puppeteer/aria_query_handler.rb +3 -2
- data/lib/puppeteer/async_utils.rb +214 -0
- data/lib/puppeteer/browser.rb +98 -56
- data/lib/puppeteer/browser_connector.rb +18 -3
- data/lib/puppeteer/browser_context.rb +196 -3
- data/lib/puppeteer/browser_runner.rb +18 -10
- data/lib/puppeteer/cdp_session.rb +67 -23
- data/lib/puppeteer/chrome_target_manager.rb +65 -40
- data/lib/puppeteer/connection.rb +55 -36
- data/lib/puppeteer/console_message.rb +9 -1
- data/lib/puppeteer/console_patch.rb +47 -0
- data/lib/puppeteer/css_coverage.rb +5 -3
- data/lib/puppeteer/custom_query_handler.rb +80 -33
- data/lib/puppeteer/define_async_method.rb +31 -37
- data/lib/puppeteer/dialog.rb +47 -14
- data/lib/puppeteer/element_handle.rb +231 -62
- data/lib/puppeteer/emulation_manager.rb +1 -1
- data/lib/puppeteer/env.rb +1 -1
- data/lib/puppeteer/errors.rb +25 -2
- data/lib/puppeteer/event_callbackable.rb +15 -0
- data/lib/puppeteer/events.rb +4 -0
- data/lib/puppeteer/execution_context.rb +148 -3
- data/lib/puppeteer/file_chooser.rb +6 -0
- data/lib/puppeteer/frame.rb +162 -91
- data/lib/puppeteer/frame_manager.rb +69 -48
- data/lib/puppeteer/http_request.rb +114 -38
- data/lib/puppeteer/http_response.rb +24 -7
- data/lib/puppeteer/isolated_world.rb +64 -41
- data/lib/puppeteer/js_coverage.rb +5 -3
- data/lib/puppeteer/js_handle.rb +58 -16
- data/lib/puppeteer/keyboard.rb +30 -17
- data/lib/puppeteer/launcher/browser_options.rb +3 -1
- data/lib/puppeteer/launcher/chrome.rb +8 -5
- data/lib/puppeteer/launcher/launch_options.rb +7 -2
- data/lib/puppeteer/launcher.rb +4 -8
- data/lib/puppeteer/lifecycle_watcher.rb +38 -22
- data/lib/puppeteer/mouse.rb +273 -64
- data/lib/puppeteer/network_event_manager.rb +7 -0
- data/lib/puppeteer/network_manager.rb +393 -112
- data/lib/puppeteer/page/screenshot_task_queue.rb +14 -4
- data/lib/puppeteer/page.rb +568 -226
- data/lib/puppeteer/puppeteer.rb +171 -64
- data/lib/puppeteer/query_handler_manager.rb +112 -16
- data/lib/puppeteer/reactor_runner.rb +247 -0
- data/lib/puppeteer/remote_object.rb +127 -47
- data/lib/puppeteer/target.rb +74 -27
- data/lib/puppeteer/task_manager.rb +3 -1
- data/lib/puppeteer/timeout_helper.rb +6 -10
- data/lib/puppeteer/touch_handle.rb +39 -0
- data/lib/puppeteer/touch_screen.rb +72 -22
- data/lib/puppeteer/tracing.rb +3 -3
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +264 -101
- data/lib/puppeteer/web_socket.rb +2 -2
- data/lib/puppeteer/web_socket_transport.rb +91 -27
- data/lib/puppeteer/web_worker.rb +175 -0
- data/lib/puppeteer.rb +20 -4
- data/puppeteer-ruby.gemspec +15 -11
- data/sig/_external.rbs +8 -0
- data/sig/_supplementary.rbs +314 -0
- data/sig/puppeteer/browser.rbs +166 -0
- data/sig/puppeteer/cdp_session.rbs +64 -0
- data/sig/puppeteer/dialog.rbs +41 -0
- data/sig/puppeteer/element_handle.rbs +305 -0
- data/sig/puppeteer/execution_context.rbs +87 -0
- data/sig/puppeteer/frame.rbs +226 -0
- data/sig/puppeteer/http_request.rbs +214 -0
- data/sig/puppeteer/http_response.rbs +89 -0
- data/sig/puppeteer/js_handle.rbs +64 -0
- data/sig/puppeteer/keyboard.rbs +40 -0
- data/sig/puppeteer/mouse.rbs +113 -0
- data/sig/puppeteer/page.rbs +515 -0
- data/sig/puppeteer/puppeteer.rbs +98 -0
- data/sig/puppeteer/remote_object.rbs +78 -0
- data/sig/puppeteer/touch_handle.rbs +21 -0
- data/sig/puppeteer/touch_screen.rbs +35 -0
- data/sig/puppeteer/web_worker.rbs +83 -0
- metadata +116 -45
- data/CHANGELOG.md +0 -397
- data/lib/puppeteer/concurrent_ruby_utils.rb +0 -81
- data/lib/puppeteer/firefox_target_manager.rb +0 -157
- data/lib/puppeteer/launcher/firefox.rb +0 -453
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
class Puppeteer::TouchHandle
|
|
4
|
+
# @rbs touchscreen: Puppeteer::TouchScreen -- Touchscreen instance
|
|
5
|
+
# @rbs touch_point: Hash[Symbol, Numeric] -- Touch point payload
|
|
6
|
+
# @rbs return: void -- No return value
|
|
7
|
+
def initialize(touchscreen, touch_point)
|
|
8
|
+
@touchscreen = touchscreen
|
|
9
|
+
@touch_point = touch_point
|
|
10
|
+
@started = false
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :touch_point
|
|
14
|
+
|
|
15
|
+
# @rbs return: void -- No return value
|
|
16
|
+
def start
|
|
17
|
+
if @started
|
|
18
|
+
raise Puppeteer::TouchError.new('Touch has already started')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@touchscreen.send(:dispatch_touch_event, 'touchStart', [@touch_point])
|
|
22
|
+
@started = true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @rbs x: Numeric -- New X coordinate
|
|
26
|
+
# @rbs y: Numeric -- New Y coordinate
|
|
27
|
+
# @rbs return: void -- No return value
|
|
28
|
+
def move(x, y)
|
|
29
|
+
@touch_point[:x] = x.round
|
|
30
|
+
@touch_point[:y] = y.round
|
|
31
|
+
@touchscreen.send(:dispatch_touch_event, 'touchMove', [@touch_point])
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs return: void -- No return value
|
|
35
|
+
def end
|
|
36
|
+
@touchscreen.send(:dispatch_touch_event, 'touchEnd', [@touch_point])
|
|
37
|
+
@touchscreen.send(:remove_handle, self)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -1,38 +1,88 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
1
3
|
class Puppeteer::TouchScreen
|
|
2
4
|
using Puppeteer::DefineAsyncMethod
|
|
3
5
|
|
|
4
|
-
# @
|
|
5
|
-
# @
|
|
6
|
+
# @rbs client: Puppeteer::CDPSession -- CDP session
|
|
7
|
+
# @rbs keyboard: Puppeteer::Keyboard -- Keyboard state for modifiers
|
|
8
|
+
# @rbs return: void -- No return value
|
|
6
9
|
def initialize(client, keyboard)
|
|
7
10
|
@client = client
|
|
8
11
|
@keyboard = keyboard
|
|
12
|
+
@touch_id_counter = 0
|
|
13
|
+
@touches = []
|
|
9
14
|
end
|
|
10
15
|
|
|
11
|
-
# @
|
|
12
|
-
# @
|
|
16
|
+
# @rbs x: Numeric -- X coordinate
|
|
17
|
+
# @rbs y: Numeric -- Y coordinate
|
|
18
|
+
# @rbs return: void -- No return value
|
|
13
19
|
def tap(x, y)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
touch = touch_start(x, y)
|
|
21
|
+
touch.end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
define_async_method :async_tap
|
|
25
|
+
|
|
26
|
+
# @rbs x: Numeric -- X coordinate
|
|
27
|
+
# @rbs y: Numeric -- Y coordinate
|
|
28
|
+
# @rbs return: Puppeteer::TouchHandle -- Touch handle
|
|
29
|
+
def touch_start(x, y)
|
|
30
|
+
@touch_id_counter += 1
|
|
31
|
+
touch_point = {
|
|
32
|
+
x: x.round,
|
|
33
|
+
y: y.round,
|
|
34
|
+
radiusX: 0.5,
|
|
35
|
+
radiusY: 0.5,
|
|
36
|
+
force: 0.5,
|
|
37
|
+
id: @touch_id_counter,
|
|
38
|
+
}
|
|
39
|
+
touch = Puppeteer::TouchHandle.new(self, touch_point)
|
|
40
|
+
touch.start
|
|
41
|
+
@touches << touch
|
|
42
|
+
touch
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
define_async_method :async_touch_start
|
|
46
|
+
|
|
47
|
+
# @rbs x: Numeric -- X coordinate
|
|
48
|
+
# @rbs y: Numeric -- Y coordinate
|
|
49
|
+
# @rbs return: void -- No return value
|
|
50
|
+
def touch_move(x, y)
|
|
51
|
+
touch = @touches.first
|
|
52
|
+
raise Puppeteer::TouchError.new('Must start a new Touch first') unless touch
|
|
53
|
+
|
|
54
|
+
touch.move(x, y)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
define_async_method :async_touch_move
|
|
21
58
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
59
|
+
# @rbs return: void -- No return value
|
|
60
|
+
def touch_end
|
|
61
|
+
touch = @touches.shift
|
|
62
|
+
raise Puppeteer::TouchError.new('Must start a new Touch first') unless touch
|
|
63
|
+
|
|
64
|
+
touch.end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
define_async_method :async_touch_end
|
|
68
|
+
|
|
69
|
+
# @rbs touch: Puppeteer::TouchHandle -- Touch handle to remove
|
|
70
|
+
# @rbs return: void -- No return value
|
|
71
|
+
private def remove_handle(touch)
|
|
72
|
+
index = @touches.index(touch)
|
|
73
|
+
return unless index
|
|
74
|
+
|
|
75
|
+
@touches.delete_at(index)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @rbs type: String -- Touch event type
|
|
79
|
+
# @rbs touch_points: Array[Hash[Symbol, Numeric]] -- Touch points payload
|
|
80
|
+
# @rbs return: void -- No return value
|
|
81
|
+
private def dispatch_touch_event(type, touch_points)
|
|
25
82
|
@client.send_message('Input.dispatchTouchEvent',
|
|
26
|
-
type:
|
|
83
|
+
type: type,
|
|
27
84
|
touchPoints: touch_points,
|
|
28
85
|
modifiers: @keyboard.modifiers,
|
|
29
86
|
)
|
|
30
|
-
@client.send_message('Input.dispatchTouchEvent',
|
|
31
|
-
type: 'touchEnd',
|
|
32
|
-
touchPoints: [],
|
|
33
|
-
modifiers: @keyboard.modifiers,
|
|
34
|
-
)
|
|
35
87
|
end
|
|
36
|
-
|
|
37
|
-
define_async_method :async_tap
|
|
38
88
|
end
|
data/lib/puppeteer/tracing.rb
CHANGED
|
@@ -41,15 +41,15 @@ class Puppeteer::Tracing
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def stop
|
|
44
|
-
stream_promise =
|
|
44
|
+
stream_promise = Async::Promise.new.tap do |future|
|
|
45
45
|
@client.once('Tracing.tracingComplete') do |event|
|
|
46
|
-
|
|
46
|
+
future.resolve(event['stream'])
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
49
|
@client.send_message('Tracing.end')
|
|
50
50
|
@recording = false
|
|
51
51
|
|
|
52
|
-
stream =
|
|
52
|
+
stream = stream_promise.wait
|
|
53
53
|
chunks = Puppeteer::ProtocolStreamReader.new(client: @client, handle: stream).read_as_chunks
|
|
54
54
|
|
|
55
55
|
StringIO.open do |stringio|
|
data/lib/puppeteer/version.rb
CHANGED
data/lib/puppeteer/wait_task.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
class Puppeteer::WaitTask
|
|
2
2
|
using Puppeteer::DefineAsyncMethod
|
|
3
3
|
|
|
4
|
-
class TerminatedError <
|
|
4
|
+
class TerminatedError < Puppeteer::Error; end
|
|
5
5
|
|
|
6
6
|
class TimeoutError < ::Puppeteer::TimeoutError
|
|
7
|
-
def initialize(
|
|
8
|
-
super("
|
|
7
|
+
def initialize(timeout:)
|
|
8
|
+
super("Waiting failed: #{timeout}ms exceeded")
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ class Puppeteer::WaitTask
|
|
|
15
15
|
raise ArgumentError.new("Unknown polling option: #{polling}")
|
|
16
16
|
end
|
|
17
17
|
elsif polling.is_a?(Numeric)
|
|
18
|
-
|
|
18
|
+
if polling < 0
|
|
19
19
|
raise ArgumentError.new("Cannot poll with non-positive interval: #{polling}")
|
|
20
20
|
end
|
|
21
21
|
else
|
|
@@ -26,7 +26,7 @@ class Puppeteer::WaitTask
|
|
|
26
26
|
@polling = polling
|
|
27
27
|
@timeout = timeout
|
|
28
28
|
@root = root
|
|
29
|
-
@predicate_body =
|
|
29
|
+
@predicate_body = build_predicate_body(predicate_body)
|
|
30
30
|
@args = args
|
|
31
31
|
@binding_function = binding_function
|
|
32
32
|
@run_count = 0
|
|
@@ -34,163 +34,326 @@ class Puppeteer::WaitTask
|
|
|
34
34
|
if binding_function
|
|
35
35
|
@dom_world.send(:_bound_functions)[binding_function.name] = binding_function
|
|
36
36
|
end
|
|
37
|
-
@promise =
|
|
37
|
+
@promise = Async::Promise.new
|
|
38
|
+
@poller_handle = nil
|
|
39
|
+
@generic_error = Puppeteer::Error.new('Waiting failed')
|
|
38
40
|
|
|
39
41
|
# Since page navigation requires us to re-install the pageScript, we should track
|
|
40
42
|
# timeout on our end.
|
|
41
43
|
if timeout && timeout > 0
|
|
42
|
-
timeout_error = TimeoutError.new(
|
|
43
|
-
|
|
44
|
+
timeout_error = TimeoutError.new(timeout: timeout)
|
|
45
|
+
@timeout_task = Async do |task|
|
|
46
|
+
task.sleep(timeout / 1000.0)
|
|
47
|
+
# Avoid stopping the timeout task from inside terminate/cleanup.
|
|
48
|
+
@timeout_task = nil
|
|
49
|
+
terminate(timeout_error) unless @timeout_cleared
|
|
50
|
+
end
|
|
44
51
|
end
|
|
52
|
+
|
|
45
53
|
async_rerun
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
# @return [Puppeteer::JSHandle]
|
|
49
57
|
def await_promise
|
|
50
|
-
@promise.
|
|
58
|
+
@promise.wait
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
def terminate(error)
|
|
61
|
+
def terminate(error = nil)
|
|
62
|
+
return if @terminated
|
|
63
|
+
|
|
54
64
|
@terminated = true
|
|
55
|
-
|
|
65
|
+
if error && !@promise.resolved?
|
|
66
|
+
@promise.reject(error)
|
|
67
|
+
end
|
|
56
68
|
cleanup
|
|
57
69
|
end
|
|
58
70
|
|
|
59
71
|
def rerun
|
|
60
72
|
run_count = (@run_count += 1)
|
|
61
|
-
context =
|
|
73
|
+
context = nil
|
|
74
|
+
success = nil
|
|
75
|
+
error = nil
|
|
62
76
|
|
|
63
77
|
return if @terminated || run_count != @run_count
|
|
64
|
-
|
|
65
|
-
@dom_world.add_binding_to_context(context, @binding_function)
|
|
66
|
-
end
|
|
67
|
-
return if @terminated || run_count != @run_count
|
|
68
|
-
|
|
78
|
+
reset_poller
|
|
69
79
|
begin
|
|
70
|
-
|
|
80
|
+
context = @dom_world.execution_context
|
|
81
|
+
if @binding_function
|
|
82
|
+
@dom_world.add_binding_to_context(context, @binding_function)
|
|
83
|
+
end
|
|
84
|
+
return if @terminated || run_count != @run_count
|
|
85
|
+
|
|
86
|
+
@poller_handle = context.evaluate_handle(
|
|
71
87
|
WAIT_FOR_PREDICATE_PAGE_FUNCTION,
|
|
72
88
|
@root,
|
|
73
89
|
@predicate_body,
|
|
74
90
|
@polling,
|
|
75
|
-
@timeout,
|
|
76
91
|
*@args,
|
|
77
92
|
)
|
|
93
|
+
success = @poller_handle.evaluate_handle('poller => poller.result()')
|
|
78
94
|
rescue => err
|
|
79
95
|
error = err
|
|
80
96
|
end
|
|
81
97
|
|
|
82
|
-
if @terminated || run_count != @run_count
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
return if @terminated || run_count != @run_count
|
|
99
|
+
|
|
100
|
+
if error
|
|
101
|
+
bad_error = get_bad_error(error)
|
|
102
|
+
if bad_error
|
|
103
|
+
@generic_error.cause = bad_error
|
|
104
|
+
terminate(@generic_error)
|
|
105
|
+
else
|
|
106
|
+
reset_poller
|
|
85
107
|
end
|
|
86
108
|
return
|
|
87
109
|
end
|
|
88
110
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if !error && (@dom_world.evaluate("s => !s", success) rescue true)
|
|
93
|
-
success.dispose
|
|
94
|
-
return
|
|
95
|
-
end
|
|
111
|
+
@promise.resolve(success) unless @promise.resolved?
|
|
112
|
+
cleanup
|
|
113
|
+
end
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
private def cleanup
|
|
116
|
+
@timeout_cleared = true
|
|
117
|
+
begin
|
|
118
|
+
@timeout_task&.stop
|
|
119
|
+
rescue StandardError
|
|
120
|
+
# Ignore errors during timeout task cleanup.
|
|
101
121
|
end
|
|
122
|
+
reset_poller
|
|
123
|
+
@dom_world.task_manager.delete(self)
|
|
124
|
+
end
|
|
102
125
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
private def reset_poller
|
|
127
|
+
poller = @poller_handle
|
|
128
|
+
@poller_handle = nil
|
|
129
|
+
return unless poller
|
|
130
|
+
|
|
131
|
+
return if @dom_world.respond_to?(:detached?) && @dom_world.detached?
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
poller.evaluate('poller => poller.stop()')
|
|
135
|
+
rescue StandardError
|
|
136
|
+
# Ignore errors during poller cleanup.
|
|
137
|
+
end
|
|
138
|
+
begin
|
|
139
|
+
poller.dispose
|
|
140
|
+
rescue StandardError
|
|
141
|
+
# Ignore errors during poller cleanup.
|
|
107
142
|
end
|
|
143
|
+
end
|
|
108
144
|
|
|
109
|
-
|
|
110
|
-
|
|
145
|
+
private def build_predicate_body(predicate_body)
|
|
146
|
+
stripped = predicate_body.to_s.strip
|
|
147
|
+
is_function =
|
|
148
|
+
stripped.start_with?('function') ||
|
|
149
|
+
stripped.start_with?('async function') ||
|
|
150
|
+
stripped.include?('=>')
|
|
151
|
+
|
|
152
|
+
if is_function
|
|
153
|
+
"return (#{predicate_body})(...args);"
|
|
111
154
|
else
|
|
112
|
-
|
|
155
|
+
"return (#{predicate_body});"
|
|
113
156
|
end
|
|
114
|
-
|
|
115
|
-
cleanup
|
|
116
157
|
end
|
|
117
158
|
|
|
118
|
-
private def
|
|
119
|
-
|
|
120
|
-
|
|
159
|
+
private def get_bad_error(error)
|
|
160
|
+
message = error.message.to_s
|
|
161
|
+
if message.include?('Execution context is not available in detached frame')
|
|
162
|
+
return Puppeteer::Error.new('Waiting failed: Frame detached')
|
|
163
|
+
end
|
|
164
|
+
return nil if message.include?('Execution context was destroyed')
|
|
165
|
+
return nil if message.include?('Cannot find context with specified id')
|
|
166
|
+
return nil if message.include?('DiscardedBrowsingContextError')
|
|
167
|
+
return nil if message.include?('Inspected target navigated or closed')
|
|
168
|
+
|
|
169
|
+
error
|
|
121
170
|
end
|
|
122
171
|
|
|
123
172
|
define_async_method :async_rerun
|
|
124
173
|
|
|
125
174
|
WAIT_FOR_PREDICATE_PAGE_FUNCTION = <<~JAVASCRIPT
|
|
126
|
-
|
|
175
|
+
function _(root, predicateBody, polling, ...args) {
|
|
127
176
|
const predicate = new Function('...args', predicateBody);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
const observedRoot = root || document;
|
|
178
|
+
if (polling === 'mutation' && typeof MutationObserver === 'undefined') {
|
|
179
|
+
polling = 'raf';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createDeferred() {
|
|
183
|
+
let resolve;
|
|
184
|
+
let reject;
|
|
185
|
+
let finished = false;
|
|
186
|
+
const promise = new Promise((res, rej) => {
|
|
187
|
+
resolve = res;
|
|
188
|
+
reject = rej;
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
promise,
|
|
192
|
+
resolve: (value) => {
|
|
193
|
+
if (finished) return;
|
|
194
|
+
finished = true;
|
|
195
|
+
resolve(value);
|
|
196
|
+
},
|
|
197
|
+
reject: (error) => {
|
|
198
|
+
if (finished) return;
|
|
199
|
+
finished = true;
|
|
200
|
+
reject(error);
|
|
201
|
+
},
|
|
202
|
+
finished: () => finished,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class MutationPoller {
|
|
207
|
+
constructor(fn, root) {
|
|
208
|
+
this.fn = fn;
|
|
209
|
+
this.root = root;
|
|
210
|
+
this.observer = null;
|
|
211
|
+
this.deferred = null;
|
|
212
|
+
}
|
|
213
|
+
async start() {
|
|
214
|
+
this.deferred = createDeferred();
|
|
215
|
+
const result = await this.fn();
|
|
216
|
+
if (result) {
|
|
217
|
+
this.deferred.resolve(result);
|
|
218
|
+
return;
|
|
150
219
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
this.observer = new MutationObserver(async () => {
|
|
221
|
+
const result = await this.fn();
|
|
222
|
+
if (!result) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
this.deferred.resolve(result);
|
|
226
|
+
await this.stop();
|
|
227
|
+
});
|
|
228
|
+
this.observer.observe(this.root, {
|
|
229
|
+
childList: true,
|
|
230
|
+
subtree: true,
|
|
231
|
+
attributes: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
async stop() {
|
|
235
|
+
if (!this.deferred) {
|
|
236
|
+
return;
|
|
155
237
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
238
|
+
if (!this.deferred.finished()) {
|
|
239
|
+
this.deferred.reject(new Error('Polling stopped'));
|
|
240
|
+
}
|
|
241
|
+
if (this.observer) {
|
|
242
|
+
this.observer.disconnect();
|
|
243
|
+
this.observer = null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
result() {
|
|
247
|
+
if (!this.deferred) {
|
|
248
|
+
return Promise.reject(new Error('Polling never started'));
|
|
249
|
+
}
|
|
250
|
+
return this.deferred.promise;
|
|
251
|
+
}
|
|
163
252
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
253
|
+
|
|
254
|
+
class RAFPoller {
|
|
255
|
+
constructor(fn) {
|
|
256
|
+
this.fn = fn;
|
|
257
|
+
this.deferred = null;
|
|
258
|
+
this.rafId = null;
|
|
259
|
+
}
|
|
260
|
+
async start() {
|
|
261
|
+
this.deferred = createDeferred();
|
|
262
|
+
const result = await this.fn();
|
|
263
|
+
if (result) {
|
|
264
|
+
this.deferred.resolve(result);
|
|
172
265
|
return;
|
|
173
266
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
267
|
+
const poll = async () => {
|
|
268
|
+
if (!this.deferred || this.deferred.finished()) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const result = await this.fn();
|
|
272
|
+
if (result) {
|
|
273
|
+
this.deferred.resolve(result);
|
|
274
|
+
await this.stop();
|
|
275
|
+
} else {
|
|
276
|
+
this.rafId = requestAnimationFrame(poll);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
this.rafId = requestAnimationFrame(poll);
|
|
280
|
+
}
|
|
281
|
+
async stop() {
|
|
282
|
+
if (!this.deferred) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!this.deferred.finished()) {
|
|
286
|
+
this.deferred.reject(new Error('Polling stopped'));
|
|
287
|
+
}
|
|
288
|
+
if (this.rafId) {
|
|
289
|
+
cancelAnimationFrame(this.rafId);
|
|
290
|
+
this.rafId = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
result() {
|
|
294
|
+
if (!this.deferred) {
|
|
295
|
+
return Promise.reject(new Error('Polling never started'));
|
|
296
|
+
}
|
|
297
|
+
return this.deferred.promise;
|
|
177
298
|
}
|
|
178
299
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
300
|
+
|
|
301
|
+
class IntervalPoller {
|
|
302
|
+
constructor(fn, ms) {
|
|
303
|
+
this.fn = fn;
|
|
304
|
+
this.ms = ms;
|
|
305
|
+
this.interval = null;
|
|
306
|
+
this.deferred = null;
|
|
307
|
+
}
|
|
308
|
+
async start() {
|
|
309
|
+
this.deferred = createDeferred();
|
|
310
|
+
const result = await this.fn();
|
|
311
|
+
if (result) {
|
|
312
|
+
this.deferred.resolve(result);
|
|
187
313
|
return;
|
|
188
314
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
315
|
+
this.interval = setInterval(async () => {
|
|
316
|
+
const result = await this.fn();
|
|
317
|
+
if (!result) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
this.deferred.resolve(result);
|
|
321
|
+
await this.stop();
|
|
322
|
+
}, this.ms);
|
|
192
323
|
}
|
|
324
|
+
async stop() {
|
|
325
|
+
if (!this.deferred) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!this.deferred.finished()) {
|
|
329
|
+
this.deferred.reject(new Error('Polling stopped'));
|
|
330
|
+
}
|
|
331
|
+
if (this.interval) {
|
|
332
|
+
clearInterval(this.interval);
|
|
333
|
+
this.interval = null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
result() {
|
|
337
|
+
if (!this.deferred) {
|
|
338
|
+
return Promise.reject(new Error('Polling never started'));
|
|
339
|
+
}
|
|
340
|
+
return this.deferred.promise;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const runner = () => predicate(...args);
|
|
345
|
+
let poller;
|
|
346
|
+
if (polling === 'raf') {
|
|
347
|
+
poller = new RAFPoller(runner);
|
|
348
|
+
} else if (polling === 'mutation') {
|
|
349
|
+
poller = new MutationPoller(runner, observedRoot);
|
|
350
|
+
} else if (typeof polling === 'number') {
|
|
351
|
+
poller = new IntervalPoller(runner, polling);
|
|
352
|
+
} else {
|
|
353
|
+
throw new Error('Unknown polling option: ' + polling);
|
|
193
354
|
}
|
|
355
|
+
poller.start();
|
|
356
|
+
return poller;
|
|
194
357
|
}
|
|
195
358
|
JAVASCRIPT
|
|
196
359
|
end
|
data/lib/puppeteer/web_socket.rb
CHANGED
|
@@ -45,7 +45,7 @@ class Puppeteer::WebSocket
|
|
|
45
45
|
rescue Errno::ECONNRESET
|
|
46
46
|
raise EOFError.new('closed by remote')
|
|
47
47
|
end
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
def dispose
|
|
50
50
|
@socket.close
|
|
51
51
|
end
|
|
@@ -75,7 +75,7 @@ class Puppeteer::WebSocket
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
class TransportError <
|
|
78
|
+
class TransportError < Puppeteer::Error; end
|
|
79
79
|
|
|
80
80
|
private def setup
|
|
81
81
|
@ready_state = STATE_CONNECTING
|