puppeteer-ruby 0.44.2 → 0.45.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 122e93a49a21ba31268f981920fb45ddf3bbc4e5b5e37705337c7f0fcfb416d8
4
- data.tar.gz: '0940d06d294a8a069d23fd02dee7a1f00b99a3a0c787b4520d8854be0103f1cf'
3
+ metadata.gz: 037ab626560f55e8fe53f7ac978eccd187132d795bdda193b9afc5d761593c77
4
+ data.tar.gz: ee5cd15db34563d127fc4d58c846d92273cb47916096de025bed43e7580a1e81
5
5
  SHA512:
6
- metadata.gz: 8253f32cb5b92f3413ba88e5179494f76b7e3c6b7a7b58e1544691586ae771ded9992cb9404f94998e72a958e6da7fbb21df4eb00e04c7446a875b0ce4cb005f
7
- data.tar.gz: 07114ec059413a71c444e467cc61a35428c5091a1cc015b1b33447f1bfae0c543fc419a36020824e40a3ad7d8e79438c7a69770a850578eea1c8e6f3ab6b4aa3
6
+ metadata.gz: a92921b6c243605d715fb3514c1965f69d2c7446236d33d45a0657a9a9a4c401a3223ee4e68eefcd8cadd6cda7cb598c845a74a152ef8ba1f980f3ea29d335d2
7
+ data.tar.gz: eb07704a147fa0029b0ac328dee0ec2a6757528058f102669cf2f8e2d6eae954f195665f1cbc4b2bc6a33e02e6b948fea18a78232c65407b9ce0c5762195c2c7
data/CHANGELOG.md CHANGED
@@ -1,6 +1,17 @@
1
- ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.44.2...main)]
1
+ ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.45.0...main)]
2
2
 
3
- - xxx
3
+ ### 0.45.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.44.3...0.45.0)]
4
+
5
+ - Port Puppeteer v18.0-v19.5 features except for 3 features below (If wanted, please create a issue of the feature request :)
6
+ - Extra headers for Puppeteer.connect ([#9314](https://github.com/puppeteer/puppeteer/pull/9314))
7
+ - Improvement of missing product message ([#9207](https://github.com/puppeteer/puppeteer/pull/9207))
8
+ - Configuration file (.puppeteerrc/puppeteer.config.js) ([#9140](https://github.com/puppeteer/puppeteer/pull/9140)).
9
+
10
+ ### 0.44.3 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.44.2...0.44.3)]
11
+
12
+ Bugfix
13
+
14
+ - Revive Firefox automation :tada:
4
15
 
5
16
  ### 0.44.2 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.44.1...0.44.2)]
6
17
 
data/README.md CHANGED
@@ -229,12 +229,6 @@ end
229
229
 
230
230
  https://yusukeiwaki.github.io/puppeteer-ruby-docs/
231
231
 
232
- ## Limitations
233
-
234
- ### Not compatible with Firefox >= v97.0
235
-
236
- :sos: Help and contribution wanted! :sos:
237
- https://github.com/YusukeIwaki/puppeteer-ruby/issues/220
238
232
 
239
233
  ## Contributing
240
234
 
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v17.1.3
3
- - puppeteer-ruby version: 0.44.2
2
+ - Puppeteer version: v19.5.0
3
+ - puppeteer-ruby version: 0.45.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -14,7 +14,8 @@
14
14
  * executablePath => `#executable_path`
15
15
  * launch
16
16
  * networkConditions => `#network_conditions`
17
- * ~~registerCustomQueryHandler~~
17
+ * ~~puppeteer~~
18
+ * registerCustomQueryHandler => `#register_custom_query_handler`
18
19
  * ~~unregisterCustomQueryHandler~~
19
20
 
20
21
  ## ~~Accessibility~~
@@ -134,6 +135,7 @@
134
135
  * screenshot
135
136
  * select
136
137
  * tap
138
+ * toElement => `#to_element`
137
139
  * type => `#type_text`
138
140
  * uploadFile => `#upload_file`
139
141
  * waitForSelector => `#wait_for_selector`
@@ -282,6 +284,7 @@
282
284
  * addScriptTag => `#add_script_tag`
283
285
  * addStyleTag => `#add_style_tag`
284
286
  * addStyleTag => `#add_style_tag`
287
+ * addStyleTag => `#add_style_tag`
285
288
  * authenticate
286
289
  * bringToFront => `#bring_to_front`
287
290
  * browser
@@ -356,6 +359,12 @@
356
359
  * waitForXPath => `#wait_for_xpath`
357
360
  * workers
358
361
 
362
+ ## ~~ProductLauncher~~
363
+
364
+ * ~~defaultArgs~~
365
+ * ~~executablePath~~
366
+ * ~~launch~~
367
+
359
368
  ## ~~ProtocolError~~
360
369
 
361
370
 
@@ -11,6 +11,8 @@ class Puppeteer::BrowserContext
11
11
  @id = context_id
12
12
  end
13
13
 
14
+ attr_reader :id
15
+
14
16
  # @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
15
17
  def on(event_name, &block)
16
18
  unless BrowserContextEmittedEvents.values.include?(event_name.to_s)
@@ -8,11 +8,13 @@ class Puppeteer::Coverage
8
8
  def start_js_coverage(
9
9
  reset_on_navigation: nil,
10
10
  report_anonymous_scripts: nil,
11
- include_raw_script_coverage: nil)
11
+ include_raw_script_coverage: nil,
12
+ use_block_coverage: nil)
12
13
  @js.start(
13
14
  reset_on_navigation: reset_on_navigation,
14
15
  report_anonymous_scripts: report_anonymous_scripts,
15
16
  include_raw_script_coverage: include_raw_script_coverage,
17
+ use_block_coverage: use_block_coverage,
16
18
  )
17
19
  end
18
20
 
@@ -24,6 +26,7 @@ class Puppeteer::Coverage
24
26
  reset_on_navigation: nil,
25
27
  report_anonymous_scripts: nil,
26
28
  include_raw_script_coverage: nil,
29
+ use_block_coverage: nil,
27
30
  &block)
28
31
  unless block
29
32
  raise ArgumentError.new('Block must be given')
@@ -33,6 +36,7 @@ class Puppeteer::Coverage
33
36
  reset_on_navigation: reset_on_navigation,
34
37
  report_anonymous_scripts: report_anonymous_scripts,
35
38
  include_raw_script_coverage: include_raw_script_coverage,
39
+ use_block_coverage: use_block_coverage,
36
40
  )
37
41
  block.call
38
42
  stop_js_coverage
@@ -126,6 +126,13 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
126
126
 
127
127
  define_async_method :async_wait_for_xpath
128
128
 
129
+ def to_element(tag_name)
130
+ unless evaluate('(node, tagName) => node.nodeName === tagName.toUpperCase()', tag_name)
131
+ raise ArgumentError.new("Element is not a(n) `#{tag_name}` element")
132
+ end
133
+ self
134
+ end
135
+
129
136
  def as_element
130
137
  self
131
138
  end
@@ -159,8 +166,31 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
159
166
  begin
160
167
  @remote_object.scroll_into_view_if_needed(@client)
161
168
  rescue => err
162
- # Just ignore 'Node does not have a layout object' for backward-compatibility.
163
- raise unless err.message =~ /Node does not have a layout object/
169
+ # Fallback to Element.scrollIntoView if DOM.scrollIntoViewIfNeeded is not supported
170
+ js = <<~JAVASCRIPT
171
+ async (element, pageJavascriptEnabled) => {
172
+ const visibleRatio = async () => {
173
+ return await new Promise(resolve => {
174
+ const observer = new IntersectionObserver(entries => {
175
+ resolve(entries[0].intersectionRatio);
176
+ observer.disconnect();
177
+ });
178
+ observer.observe(element);
179
+ });
180
+ };
181
+ if (!pageJavascriptEnabled || (await visibleRatio()) !== 1.0) {
182
+ element.scrollIntoView({
183
+ block: 'center',
184
+ inline: 'center',
185
+ // @ts-expect-error Chrome still supports behavior: instant but
186
+ // it's not in the spec so TS shouts We don't want to make this
187
+ // breaking change in Puppeteer yet so we'll ignore the line.
188
+ behavior: 'instant',
189
+ });
190
+ }
191
+ }
192
+ JAVASCRIPT
193
+ evaluate(js, page.javascript_enabled?)
164
194
  end
165
195
 
166
196
  # clickpoint is often calculated before scrolling is completed.
@@ -118,12 +118,12 @@ class Puppeteer::ExecutionContext
118
118
  ) # .catch(rewriteError);
119
119
 
120
120
  exception_details = result['exceptionDetails']
121
- remote_object = Puppeteer::RemoteObject.new(result['result'])
122
-
123
121
  if exception_details
124
122
  raise EvaluationError.new("Evaluation failed: #{exception_details}")
125
123
  end
126
124
 
125
+ remote_object = Puppeteer::RemoteObject.new(result['result'])
126
+
127
127
  if @return_by_value
128
128
  remote_object.value
129
129
  else
@@ -66,9 +66,8 @@ class Puppeteer::FirefoxTargetManager
66
66
  end
67
67
 
68
68
  private def remove_session_listeners(session)
69
- return unless @attachment_listener_ids
70
- listener_ids = @attachment_listener_ids.delete(session)
71
- return if listener_ids.empty?
69
+ listener_ids = @attachment_listener_ids&.delete(session)
70
+ return if !listener_ids || listener_ids.empty?
72
71
  session.remove_event_listener(*listener_ids)
73
72
  end
74
73
 
@@ -310,7 +310,7 @@ class Puppeteer::Frame
310
310
 
311
311
  # Ensure loaderId updated.
312
312
  # The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
313
- @loader_id = frame_payload['loaderId']
313
+ @loader_id = frame_payload['loaderId'] if frame_payload['loaderId']
314
314
  end
315
315
 
316
316
  # @param url [String]
@@ -21,6 +21,8 @@ class Puppeteer::FrameManager
21
21
  # @type {!Map<string, !Frame>}
22
22
  @frames = {}
23
23
 
24
+ @frame_naviigated_received = Set.new
25
+
24
26
  # @type {!Map<number, !ExecutionContext>}
25
27
  @context_id_to_context = {}
26
28
 
@@ -42,6 +44,7 @@ class Puppeteer::FrameManager
42
44
  handle_frame_attached(client, event['frameId'], event['parentFrameId'])
43
45
  end
44
46
  client.on_event('Page.frameNavigated') do |event|
47
+ @frame_naviigated_received << event['frame']['id']
45
48
  handle_frame_navigated(event['frame'])
46
49
  end
47
50
  client.on_event('Page.navigatedWithinDocument') do |event|
@@ -227,7 +230,9 @@ class Puppeteer::FrameManager
227
230
  if frame_tree['frame']['parentId']
228
231
  handle_frame_attached(session, frame_tree['frame']['id'], frame_tree['frame']['parentId'])
229
232
  end
230
- handle_frame_navigated(frame_tree['frame'])
233
+ unless @frame_naviigated_received.delete?(frame_tree['frame']['id'])
234
+ handle_frame_navigated(frame_tree['frame'])
235
+ end
231
236
  return if !frame_tree['childFrames']
232
237
 
233
238
  frame_tree['childFrames'].each do |child|
@@ -57,7 +57,7 @@ class Puppeteer::IsolaatedWorld
57
57
  @frame = frame
58
58
  @timeout_settings = timeout_settings
59
59
  @context_promise = resolvable_future
60
- @wait_tasks = Set.new
60
+ @task_manager = Puppeteer::TaskManager.new
61
61
  @bound_functions = {}
62
62
  @ctx_bindings = Set.new
63
63
  @detached = false
@@ -65,12 +65,7 @@ class Puppeteer::IsolaatedWorld
65
65
  @client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
66
66
  end
67
67
 
68
- attr_reader :frame
69
-
70
- # only used in Puppeteer::WaitTask#initialize
71
- private def _wait_tasks
72
- @wait_tasks
73
- end
68
+ attr_reader :frame, :task_manager
74
69
 
75
70
  # only used in Puppeteer::WaitTask#initialize
76
71
  private def _bound_functions
@@ -84,7 +79,7 @@ class Puppeteer::IsolaatedWorld
84
79
  unless @context_promise.resolved?
85
80
  @context_promise.fulfill(context)
86
81
  end
87
- @wait_tasks.each(&:async_rerun)
82
+ @task_manager.async_rerun_all
88
83
  else
89
84
  raise ArgumentError.new("context should now be nil. Use #delete_context for clearing document.")
90
85
  end
@@ -101,9 +96,7 @@ class Puppeteer::IsolaatedWorld
101
96
 
102
97
  def detach
103
98
  @detached = true
104
- @wait_tasks.each do |wait_task|
105
- wait_task.terminate(Puppeteer::WaitTask::TerminatedError.new('waitForFunction failed: frame got detached.'))
106
- end
99
+ @task_manager.terminate_all(Puppeteer::WaitTask::TerminatedError.new('waitForFunction failed: frame got detached.'))
107
100
  end
108
101
 
109
102
  class DetachedError < StandardError; end
@@ -569,18 +562,16 @@ class Puppeteer::IsolaatedWorld
569
562
  node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
570
563
 
571
564
  const style = window.getComputedStyle(element);
565
+ const hidden_visibility_values = ['hidden', 'collapse'];
572
566
  const isVisible =
573
- style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
567
+ style && !hidden_visibility_values.includes(style.visibility) && !isBoundingBoxEmpty(element);
574
568
  const success =
575
569
  waitForVisible === isVisible || waitForHidden === !isVisible;
576
570
  return success ? node : null;
577
571
 
578
- /**
579
- * @return {boolean}
580
- */
581
- function hasVisibleBoundingBox() {
572
+ function isBoundingBoxEmpty(element) {
582
573
  const rect = element.getBoundingClientRect();
583
- return !!(rect.top || rect.bottom || rect.width || rect.height);
574
+ return rect.width === 0 || rect.height === 0;
584
575
  }
585
576
  }
586
577
  }
@@ -31,7 +31,8 @@ class Puppeteer::JSCoverage
31
31
  def start(
32
32
  reset_on_navigation: nil,
33
33
  report_anonymous_scripts: nil,
34
- include_raw_script_coverage: nil)
34
+ include_raw_script_coverage: nil,
35
+ use_block_coverage: nil)
35
36
  raise 'JSCoverage is already enabled' if @enabled
36
37
 
37
38
  @reset_on_navigation =
@@ -40,6 +41,12 @@ class Puppeteer::JSCoverage
40
41
  else
41
42
  true
42
43
  end
44
+ @use_block_coverage =
45
+ if [true, false].include?(use_block_coverage)
46
+ use_block_coverage
47
+ else
48
+ true
49
+ end
43
50
  @report_anonymous_scripts = report_anonymous_scripts || false
44
51
  @include_raw_script_coverage = include_raw_script_coverage || false
45
52
  @enabled = true
@@ -56,7 +63,7 @@ class Puppeteer::JSCoverage
56
63
  @client.async_send_message('Profiler.enable'),
57
64
  @client.async_send_message('Profiler.startPreciseCoverage',
58
65
  callCount: @include_raw_script_coverage,
59
- detailed: true,
66
+ detailed: @use_block_coverage,
60
67
  ),
61
68
  @client.async_send_message('Debugger.enable'),
62
69
  @client.async_send_message('Debugger.setSkipAllPauses', skip: true),
@@ -15,7 +15,7 @@ class Puppeteer::Keyboard
15
15
 
16
16
  # @param key [String]
17
17
  # @param text [String]
18
- def down(key, text: nil)
18
+ def down(key, text: nil, commands: nil)
19
19
  description = key_description_for_string(key)
20
20
 
21
21
  auto_repeat = @pressed_keys.include?(description.code)
@@ -34,6 +34,7 @@ class Puppeteer::Keyboard
34
34
  autoRepeat: auto_repeat,
35
35
  location: description.location,
36
36
  isKeypad: description.location == 3,
37
+ commands: commands,
37
38
  }.compact
38
39
  @client.send_message('Input.dispatchKeyEvent', params)
39
40
  end
@@ -151,8 +152,8 @@ class Puppeteer::Keyboard
151
152
  # @param key [String]
152
153
  # @param text [String]
153
154
  # @return [Future]
154
- def press(key, delay: nil, text: nil)
155
- down(key, text: text)
155
+ def press(key, delay: nil, text: nil, commands: nil)
156
+ down(key, text: text, commands: commands)
156
157
  if delay
157
158
  sleep(delay.to_i / 1000.0)
158
159
  end
@@ -114,36 +114,38 @@ module Puppeteer::Launcher
114
114
 
115
115
  # @param options [Launcher::ChromeArgOptions]
116
116
  def initialize(chrome_arg_options)
117
+ # See https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
117
118
  chrome_arguments = [
119
+ '--allow-pre-commit-input',
118
120
  '--disable-background-networking',
119
- '--enable-features=NetworkService,NetworkServiceInProcess',
120
121
  '--disable-background-timer-throttling',
121
122
  '--disable-backgrounding-occluded-windows',
122
123
  '--disable-breakpad',
123
124
  '--disable-client-side-phishing-detection',
124
125
  '--disable-component-extensions-with-background-pages',
126
+ '--disable-component-update',
125
127
  '--disable-default-apps',
126
128
  '--disable-dev-shm-usage',
127
129
  '--disable-extensions',
128
- # TODO: remove AvoidUnnecessaryBeforeUnloadCheckSync below
129
- # once crbug.com/1324138 is fixed and released.
130
130
  # AcceptCHFrame disabled because of crbug.com/1348106.
131
- '--disable-features=Translate,BackForwardCache,AcceptCHFrame,AvoidUnnecessaryBeforeUnloadCheckSync',
131
+ '--disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints',
132
132
  '--disable-hang-monitor',
133
133
  '--disable-ipc-flooding-protection',
134
134
  '--disable-popup-blocking',
135
135
  '--disable-prompt-on-repost',
136
136
  '--disable-renderer-backgrounding',
137
137
  '--disable-sync',
138
+ '--enable-automation',
139
+ # TODO(sadym): remove '--enable-blink-features=IdleDetection' once
140
+ # IdleDetection is turned on by default.
141
+ '--enable-blink-features=IdleDetection',
142
+ '--enable-features=NetworkServiceInProcess2',
143
+ '--export-tagged-pdf',
138
144
  '--force-color-profile=srgb',
139
145
  '--metrics-recording-only',
140
146
  '--no-first-run',
141
- '--enable-automation',
142
147
  '--password-store=basic',
143
148
  '--use-mock-keychain',
144
- # TODO(sadym): remove '--enable-blink-features=IdleDetection'
145
- # once IdleDetection is turned on by default.
146
- '--enable-blink-features=IdleDetection',
147
149
  ]
148
150
 
149
151
  if chrome_arg_options.user_data_dir
@@ -324,6 +324,11 @@ module Puppeteer::Launcher
324
324
  # Make sure opening about:addons will not hit the network
325
325
  'extensions.webservice.discoverURL': "http://#{server}/dummy/discoveryURL",
326
326
 
327
+ # Temporarily force disable BFCache in parent (https://bit.ly/bug-1732263)
328
+ 'fission.bfcacheInParent': false,
329
+ # Force all web content to use a single content process
330
+ 'fission.webContentIsolationStrategy': 0,
331
+
327
332
  # Allow the application to have focus even it runs in the background
328
333
  'focusmanager.testmode': true,
329
334
  # Disable useragent updates
@@ -84,6 +84,7 @@ class Puppeteer::LifecycleWatcher
84
84
  @listener_ids['network_manager'] = [
85
85
  @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Request, &method(:handle_request)),
86
86
  @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::Response, &method(:handle_response)),
87
+ @frame_manager.network_manager.add_event_listener(NetworkManagerEmittedEvents::RequestFailed, &method(:handle_request_failed)),
87
88
  ]
88
89
 
89
90
  @same_document_navigation_promise = resolvable_future
@@ -107,9 +108,16 @@ class Puppeteer::LifecycleWatcher
107
108
  end
108
109
  end
109
110
 
111
+ # @param [Puppeteer::HTTPRequest] request
112
+ def handle_request_failed(request)
113
+ return if @navigation_request&.internal&.request_id != request.internal.request_id
114
+
115
+ @navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
116
+ end
117
+
110
118
  # @param [Puppeteer::HTTPResponse] response
111
119
  def handle_response(response)
112
- return if @navigation_request&.internal&.request_id != response.request.internal.request_id
120
+ return if @navigation_request&.internal&.request_id != response.request.internal.request_id
113
121
 
114
122
  @navigation_response_received.fulfill(nil) unless @navigation_response_received.resolved?
115
123
  end
@@ -105,15 +105,10 @@ class Puppeteer::Page
105
105
  else
106
106
  options[:capture_beyond_viewport]
107
107
  end
108
- @from_surface =
109
- if options[:from_surface].nil?
110
- true
111
- else
112
- options[:from_surface]
113
- end
108
+ @from_surface = options[:from_surface]
114
109
  end
115
110
 
116
- attr_reader :type, :quality, :path, :clip, :encoding
111
+ attr_reader :type, :quality, :path, :clip, :encoding, :from_surface
117
112
 
118
113
  def full_page?
119
114
  @full_page
@@ -126,9 +121,5 @@ class Puppeteer::Page
126
121
  def capture_beyond_viewport?
127
122
  @capture_beyond_viewport
128
123
  end
129
-
130
- def from_surface?
131
- @from_surface
132
- end
133
124
  end
134
125
  end
@@ -1104,7 +1104,7 @@ class Puppeteer::Page
1104
1104
  quality: screenshot_options.quality,
1105
1105
  clip: clip,
1106
1106
  captureBeyondViewport: screenshot_options.capture_beyond_viewport?,
1107
- fromSurface: screenshot_options.from_surface?,
1107
+ fromSurface: screenshot_options.from_surface,
1108
1108
  }.compact
1109
1109
  result = @client.send_message('Page.captureScreenshot', screenshot_params)
1110
1110
  reset_default_background_color if should_set_default_background
@@ -141,6 +141,42 @@ class Puppeteer::Puppeteer
141
141
  launcher.product
142
142
  end
143
143
 
144
+ def register_custom_query_handler(name:, query_one:, query_all:)
145
+ unless name =~ /\A[a-zA-Z]+\z/
146
+ raise ArgumentError.new("Custom query handler names may only contain [a-zA-Z]")
147
+ end
148
+
149
+ handler_name = name.to_sym
150
+ if query_handler_manager.query_handlers.key?(handler_name)
151
+ raise ArgumentError.new("A query handler named #{name} already exists")
152
+ end
153
+
154
+ handler = Puppeteer::CustomQueryHandler.new(query_one: query_one, query_all: query_all)
155
+ Puppeteer::QueryHandlerManager.instance.query_handlers[handler_name] = handler
156
+ end
157
+
158
+ def with_custom_query_handler(name:, query_one:, query_all:, &block)
159
+ unless name =~ /\A[a-zA-Z]+\z/
160
+ raise ArgumentError.new("Custom query handler names may only contain [a-zA-Z]")
161
+ end
162
+
163
+ handler_name = name.to_sym
164
+
165
+ handler = Puppeteer::CustomQueryHandler.new(query_one: query_one, query_all: query_all)
166
+ query_handler_manager = Puppeteer::QueryHandlerManager.instance
167
+ original = query_handler_manager.query_handlers.delete(handler_name)
168
+ query_handler_manager.query_handlers[handler_name] = handler
169
+ begin
170
+ block.call
171
+ ensure
172
+ if original
173
+ query_handler_manager.query_handlers[handler_name] = original
174
+ else
175
+ query_handler_manager.query_handlers.delete(handler_name)
176
+ end
177
+ end
178
+ end
179
+
144
180
  # @return [Puppeteer::Devices]
145
181
  def devices
146
182
  Puppeteer::Devices
@@ -170,10 +206,4 @@ class Puppeteer::Puppeteer
170
206
  }.compact
171
207
  launcher.default_args(options)
172
208
  end
173
-
174
- # @param {!BrowserFetcher.Options=} options
175
- # @return {!BrowserFetcher}
176
- def createBrowserFetcher(options = {})
177
- BrowserFetcher.new(@project_root, options)
178
- end
179
209
  end
@@ -7,6 +7,7 @@ class Puppeteer::QueryHandlerManager
7
7
  @query_handlers ||= {
8
8
  aria: Puppeteer::AriaQueryHandler.new,
9
9
  xpath: xpath_handler,
10
+ text: text_query_handler,
10
11
  }
11
12
  end
12
13
 
@@ -51,6 +52,156 @@ class Puppeteer::QueryHandlerManager
51
52
  )
52
53
  end
53
54
 
55
+ private def text_query_handler
56
+ text_content_js = <<~JAVASCRIPT
57
+ const TRIVIAL_VALUE_INPUT_TYPES = new Set(['checkbox', 'image', 'radio']);
58
+
59
+ /**
60
+ * Determines if the node has a non-trivial value property.
61
+ */
62
+ const isNonTrivialValueNode = (node) => {
63
+ if (node instanceof HTMLSelectElement) {
64
+ return true;
65
+ }
66
+ if (node instanceof HTMLTextAreaElement) {
67
+ return true;
68
+ }
69
+ if (
70
+ node instanceof HTMLInputElement &&
71
+ !TRIVIAL_VALUE_INPUT_TYPES.has(node.type)
72
+ ) {
73
+ return true;
74
+ }
75
+ return false;
76
+ };
77
+
78
+ const UNSUITABLE_NODE_NAMES = new Set(['SCRIPT', 'STYLE']);
79
+
80
+ /**
81
+ * Determines whether a given node is suitable for text matching.
82
+ */
83
+ const isSuitableNodeForTextMatching = (node) => {
84
+ return (
85
+ !UNSUITABLE_NODE_NAMES.has(node.nodeName) && !document.head?.contains(node)
86
+ );
87
+ };
88
+
89
+ /**
90
+ * Maps {@link Node}s to their computed {@link TextContent}.
91
+ */
92
+ const textContentCache = new Map();
93
+
94
+ /**
95
+ * Builds the text content of a node using some custom logic.
96
+ *
97
+ * @remarks
98
+ * The primary reason this function exists is due to {@link ShadowRoot}s not having
99
+ * text content.
100
+ *
101
+ * @internal
102
+ */
103
+ const createTextContent = (root) => {
104
+ let value = textContentCache.get(root);
105
+ if (value) {
106
+ return value;
107
+ }
108
+ value = {full: '', immediate: []};
109
+ if (!isSuitableNodeForTextMatching(root)) {
110
+ return value;
111
+ }
112
+ let currentImmediate = '';
113
+ if (isNonTrivialValueNode(root)) {
114
+ value.full = root.value;
115
+ value.immediate.push(root.value);
116
+ } else {
117
+ for (let child = root.firstChild; child; child = child.nextSibling) {
118
+ if (child.nodeType === Node.TEXT_NODE) {
119
+ value.full += child.nodeValue ?? '';
120
+ currentImmediate += child.nodeValue ?? '';
121
+ continue;
122
+ }
123
+ if (currentImmediate) {
124
+ value.immediate.push(currentImmediate);
125
+ }
126
+ currentImmediate = '';
127
+ if (child.nodeType === Node.ELEMENT_NODE) {
128
+ value.full += createTextContent(child).full;
129
+ }
130
+ }
131
+ if (currentImmediate) {
132
+ value.immediate.push(currentImmediate);
133
+ }
134
+ if (root instanceof Element && root.shadowRoot) {
135
+ value.full += createTextContent(root.shadowRoot).full;
136
+ }
137
+ }
138
+ textContentCache.set(root, value);
139
+ return value;
140
+ };
141
+ JAVASCRIPT
142
+
143
+ @text_query_handler ||= Puppeteer::CustomQueryHandler.new(
144
+ query_one: <<~JAVASCRIPT,
145
+ (element, selector) => {
146
+ #{text_content_js}
147
+
148
+ const search = (root) => {
149
+ for (const node of root.childNodes) {
150
+ if (node instanceof Element) {
151
+ let matchedNode;
152
+ if (node.shadowRoot) {
153
+ matchedNode = search(node.shadowRoot);
154
+ } else {
155
+ matchedNode = search(node);
156
+ }
157
+ if (matchedNode) {
158
+ return matchedNode;
159
+ }
160
+ }
161
+ }
162
+ const textContent = createTextContent(root);
163
+ if (textContent.full.includes(selector)) {
164
+ return root;
165
+ }
166
+ return null;
167
+ };
168
+ return search(element);
169
+ }
170
+ JAVASCRIPT
171
+
172
+ query_all: <<~JAVASCRIPT,
173
+ (element, selector) => {
174
+ #{text_content_js}
175
+
176
+ const search = (root) => {
177
+ let results = [];
178
+ for (const node of root.childNodes) {
179
+ if (node instanceof Element) {
180
+ let matchedNodes;
181
+ if (node.shadowRoot) {
182
+ matchedNodes = search(node.shadowRoot);
183
+ } else {
184
+ matchedNodes = search(node);
185
+ }
186
+ results = results.concat(matchedNodes);
187
+ }
188
+ }
189
+ if (results.length > 0) {
190
+ return results;
191
+ }
192
+
193
+ const textContent = createTextContent(root);
194
+ if (textContent.full.includes(selector)) {
195
+ return [root];
196
+ }
197
+ return [];
198
+ };
199
+ return search(element);
200
+ }
201
+ JAVASCRIPT
202
+ )
203
+ end
204
+
54
205
  class Result
55
206
  def initialize(query_handler:, selector:)
56
207
  @query_handler = query_handler
@@ -0,0 +1,24 @@
1
+ class Puppeteer::TaskManager
2
+ def initialize
3
+ @tasks = Set.new
4
+ end
5
+
6
+ def add(task)
7
+ @tasks << task
8
+ end
9
+
10
+ def delete(task)
11
+ @tasks.delete(task)
12
+ end
13
+
14
+ def terminate_all(error)
15
+ @tasks.each do |task|
16
+ task.terminate(error)
17
+ end
18
+ @tasks.clear
19
+ end
20
+
21
+ def async_rerun_all
22
+ Concurrent::Promises.zip(*@tasks.map(&:async_rerun))
23
+ end
24
+ end
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.44.2'
2
+ VERSION = '0.45.0'
3
3
  end
@@ -30,7 +30,7 @@ class Puppeteer::WaitTask
30
30
  @args = args
31
31
  @binding_function = binding_function
32
32
  @run_count = 0
33
- @dom_world.send(:_wait_tasks).add(self)
33
+ @dom_world.task_manager.add(self)
34
34
  if binding_function
35
35
  @dom_world.send(:_bound_functions)[binding_function.name] = binding_function
36
36
  end
@@ -117,10 +117,10 @@ class Puppeteer::WaitTask
117
117
 
118
118
  private def cleanup
119
119
  @timeout_cleared = true
120
- @dom_world.send(:_wait_tasks).delete(self)
120
+ @dom_world.task_manager.delete(self)
121
121
  end
122
122
 
123
- private define_async_method :async_rerun
123
+ define_async_method :async_rerun
124
124
 
125
125
  WAIT_FOR_PREDICATE_PAGE_FUNCTION = <<~JAVASCRIPT
126
126
  async function _(root, predicateBody, polling, timeout, ...args) {
data/lib/puppeteer.rb CHANGED
@@ -58,6 +58,7 @@ require 'puppeteer/puppeteer'
58
58
  require 'puppeteer/query_handler_manager'
59
59
  require 'puppeteer/remote_object'
60
60
  require 'puppeteer/target'
61
+ require 'puppeteer/task_manager'
61
62
  require 'puppeteer/tracing'
62
63
  require 'puppeteer/timeout_helper'
63
64
  require 'puppeteer/timeout_settings'
@@ -24,15 +24,15 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'concurrent-ruby', '~> 1.1.0'
25
25
  spec.add_dependency 'websocket-driver', '>= 0.6.0'
26
26
  spec.add_dependency 'mime-types', '>= 3.0'
27
- spec.add_development_dependency 'bundler', '~> 2.3.4'
27
+ spec.add_development_dependency 'bundler'
28
28
  spec.add_development_dependency 'chunky_png'
29
29
  spec.add_development_dependency 'dry-inflector'
30
30
  spec.add_development_dependency 'pry-byebug'
31
31
  spec.add_development_dependency 'rake', '~> 13.0.3'
32
32
  spec.add_development_dependency 'rollbar'
33
- spec.add_development_dependency 'rspec', '~> 3.11.0'
33
+ spec.add_development_dependency 'rspec', '~> 3.12.0'
34
34
  spec.add_development_dependency 'rspec_junit_formatter' # for CircleCI.
35
- spec.add_development_dependency 'rubocop', '~> 1.36.0'
35
+ spec.add_development_dependency 'rubocop', '~> 1.42.0'
36
36
  spec.add_development_dependency 'rubocop-rspec'
37
37
  spec.add_development_dependency 'sinatra'
38
38
  spec.add_development_dependency 'webrick'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppeteer-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.44.2
4
+ version: 0.45.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - YusukeIwaki
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-10-06 00:00:00.000000000 Z
11
+ date: 2023-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -56,16 +56,16 @@ dependencies:
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 2.3.4
61
+ version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 2.3.4
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: chunky_png
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: 3.11.0
145
+ version: 3.12.0
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: 3.11.0
152
+ version: 3.12.0
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: rspec_junit_formatter
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -170,14 +170,14 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 1.36.0
173
+ version: 1.42.0
174
174
  type: :development
175
175
  prerelease: false
176
176
  version_requirements: !ruby/object:Gem::Requirement
177
177
  requirements:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
- version: 1.36.0
180
+ version: 1.42.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: rubocop-rspec
183
183
  requirement: !ruby/object:Gem::Requirement
@@ -257,7 +257,6 @@ files:
257
257
  - lib/puppeteer/browser.rb
258
258
  - lib/puppeteer/browser_connector.rb
259
259
  - lib/puppeteer/browser_context.rb
260
- - lib/puppeteer/browser_fetcher.rb
261
260
  - lib/puppeteer/browser_runner.rb
262
261
  - lib/puppeteer/cdp_session.rb
263
262
  - lib/puppeteer/chrome_target_manager.rb
@@ -321,6 +320,7 @@ files:
321
320
  - lib/puppeteer/query_handler_manager.rb
322
321
  - lib/puppeteer/remote_object.rb
323
322
  - lib/puppeteer/target.rb
323
+ - lib/puppeteer/task_manager.rb
324
324
  - lib/puppeteer/timeout_helper.rb
325
325
  - lib/puppeteer/timeout_settings.rb
326
326
  - lib/puppeteer/touch_screen.rb
@@ -350,7 +350,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
350
350
  - !ruby/object:Gem::Version
351
351
  version: '0'
352
352
  requirements: []
353
- rubygems_version: 3.3.7
353
+ rubygems_version: 3.4.1
354
354
  signing_key:
355
355
  specification_version: 4
356
356
  summary: A ruby port of puppeteer
@@ -1,6 +0,0 @@
1
- # Download latest chromium.
2
- class Puppeteer::BrowserFetcher
3
- def initialize(project_root, options = {})
4
- # 未実装
5
- end
6
- end