puppeteer-ruby 0.40.7 → 0.41.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: 674e5731f3d469805214a3ddb7e56ae83aaaa6850db3ccddd02002cffe4e1ca4
4
- data.tar.gz: 47f5ad6e1a7b30c394ce950991c067c9b2dd42c144ec561eaa3dbbc9efb9b77a
3
+ metadata.gz: ccfb160c40e2ef032c0da17ab4b1cc9fe610d4caf42f236042ea0757c9e270e7
4
+ data.tar.gz: 95a3a457aa850116bc3062e9507ef53bd10e1d451ebeb4703cf9a6f5d663b9c5
5
5
  SHA512:
6
- metadata.gz: b4f53e004dbbba48ebfaa5a1197d5bfdf9d2364a6bd84edf94d378b4b193661f0dc43de51fd61e009d91d5ca1eff7afb62e824fc23ce6bfc6dfa2b36aeda0457
7
- data.tar.gz: 537e1dc87b4fa9d7644035e1a7a590ec32d64275ee7166940a9e6454b94997b873b1e06c1f2f8ad25c33f39caed4eb05601de46808f09748e13f9db15868c750
6
+ metadata.gz: b65da9f89cc8cfeea05e4e0835f919aab382f47e380d27c1e0a439c70cb0b0b7d9e8519a3361ad545e0c5115216d698740e46f20c96960cf96496ee22ac374ea
7
+ data.tar.gz: 81c846c795efd81bc10af84b65e02a6dfc3a52d9be2cc5f21cc7bc313e84fcd4ef3fed692bc1bc616389bc43f2a06a6944c2353aa8f8c3a614bb8a4c8ef2097c
data/CHANGELOG.md CHANGED
@@ -1,7 +1,11 @@
1
- ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.7...main)]
1
+ ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.41.0...main)]
2
2
 
3
3
  - xxx
4
4
 
5
+ ### 0.41.0 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.7...0.41.0)]
6
+
7
+ - Port Puppeteer v14.0-v15.2 features, including `ElementHandle#wait_for_xpath`
8
+
5
9
  ### 0.40.7 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.6...0.40.7)]
6
10
 
7
11
  - Port Puppeteer v13.6-v13.7 features.
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v13.5.2
3
- - puppeteer-ruby version: 0.40.7
2
+ - Puppeteer version: v15.2.0
3
+ - puppeteer-ruby version: 0.41.0
4
4
 
5
5
  ## Puppeteer
6
6
 
@@ -297,6 +297,7 @@
297
297
  * type => `#type_text`
298
298
  * uploadFile => `#upload_file`
299
299
  * waitForSelector => `#wait_for_selector`
300
+ * waitForXPath => `#wait_for_xpath`
300
301
 
301
302
  ## HTTPRequest
302
303
 
@@ -13,7 +13,14 @@ class Puppeteer::Browser
13
13
  # @param {?Puppeteer.Viewport} defaultViewport
14
14
  # @param process [Puppeteer::BrowserRunner::BrowserProcess|NilClass]
15
15
  # @param {function()=} closeCallback
16
- def self.create(connection:, context_ids:, ignore_https_errors:, default_viewport:, process:, close_callback:)
16
+ def self.create(connection:,
17
+ context_ids:,
18
+ ignore_https_errors:,
19
+ default_viewport:,
20
+ process:,
21
+ close_callback:,
22
+ target_filter_callback:,
23
+ is_page_target_callback:)
17
24
  browser = Puppeteer::Browser.new(
18
25
  connection: connection,
19
26
  context_ids: context_ids,
@@ -21,6 +28,8 @@ class Puppeteer::Browser
21
28
  default_viewport: default_viewport,
22
29
  process: process,
23
30
  close_callback: close_callback,
31
+ target_filter_callback: target_filter_callback,
32
+ is_page_target_callback: is_page_target_callback,
24
33
  )
25
34
  connection.send_message('Target.setDiscoverTargets', discover: true)
26
35
  browser
@@ -32,12 +41,21 @@ class Puppeteer::Browser
32
41
  # @param {?Puppeteer.Viewport} defaultViewport
33
42
  # @param {?Puppeteer.ChildProcess} process
34
43
  # @param {(function():Promise)=} closeCallback
35
- def initialize(connection:, context_ids:, ignore_https_errors:, default_viewport:, process:, close_callback:)
44
+ def initialize(connection:,
45
+ context_ids:,
46
+ ignore_https_errors:,
47
+ default_viewport:,
48
+ process:,
49
+ close_callback:,
50
+ target_filter_callback:,
51
+ is_page_target_callback:)
36
52
  @ignore_https_errors = ignore_https_errors
37
53
  @default_viewport = default_viewport
38
54
  @process = process
39
55
  @connection = connection
40
56
  @close_callback = close_callback
57
+ @target_filter_callback = target_filter_callback || method(:default_target_filter_callback)
58
+ @is_page_target_callback = is_page_target_callback || method(:default_is_page_target_callback)
41
59
 
42
60
  @default_context = Puppeteer::BrowserContext.new(@connection, self, nil)
43
61
  @contexts = {}
@@ -54,6 +72,16 @@ class Puppeteer::Browser
54
72
  @connection.on_event('Target.targetInfoChanged', &method(:handle_target_info_changed))
55
73
  end
56
74
 
75
+ private def default_target_filter_callback(target_info)
76
+ true
77
+ end
78
+
79
+ private def default_is_page_target_callback(target_info)
80
+ ['page', 'background_page', 'webview'].include?(target_info.type)
81
+ end
82
+
83
+ attr_reader :is_page_target_callback
84
+
57
85
  # @param event_name [Symbol] either of :disconnected, :targetcreated, :targetchanged, :targetdestroyed
58
86
  def on(event_name, &block)
59
87
  unless BrowserEmittedEvents.values.include?(event_name.to_s)
@@ -119,12 +147,16 @@ class Puppeteer::Browser
119
147
  if @targets[target_info.target_id]
120
148
  raise TargetAlreadyExistError.new
121
149
  end
150
+
151
+ return unless @target_filter_callback.call(target_info)
152
+
122
153
  target = Puppeteer::Target.new(
123
154
  target_info: target_info,
124
155
  browser_context: context,
125
156
  session_factory: -> { @connection.create_session(target_info) },
126
157
  ignore_https_errors: @ignore_https_errors,
127
158
  default_viewport: @default_viewport,
159
+ is_page_target_callback: @is_page_target_callback,
128
160
  )
129
161
  @targets[target_info.target_id] = target
130
162
  if_present(@wait_for_creating_targets.delete(target_info.target_id)) do |promise|
@@ -50,7 +50,9 @@ class Puppeteer::BrowserContext
50
50
 
51
51
  # @return {!Promise<!Array<!Puppeteer.Page>>}
52
52
  def pages
53
- targets.select { |target| target.type == 'page' }.map(&:page).reject { |page| !page }
53
+ targets.select { |target|
54
+ target.type == 'page' || (target.type == 'other' && @browser.is_page_target_callback&.call(target.target_info))
55
+ }.map(&:page).reject { |page| !page }
54
56
  end
55
57
 
56
58
  def incognito?
@@ -76,6 +76,81 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
76
76
 
77
77
  define_async_method :async_wait_for_selector
78
78
 
79
+ # Wait for the `xpath` within the element. If at the moment of calling the
80
+ # method the `xpath` already exists, the method will return immediately. If
81
+ # the `xpath` doesn't appear after the `timeout` milliseconds of waiting, the
82
+ # function will throw.
83
+ #
84
+ # If `xpath` starts with `//` instead of `.//`, the dot will be appended automatically.
85
+ #
86
+ # This method works across navigation
87
+ # ```js
88
+ # const puppeteer = require('puppeteer');
89
+ # (async () => {
90
+ # const browser = await puppeteer.launch();
91
+ # const page = await browser.newPage();
92
+ # let currentURL;
93
+ # page
94
+ # .waitForXPath('//img')
95
+ # .then(() => console.log('First URL with image: ' + currentURL));
96
+ # for (currentURL of [
97
+ # 'https://example.com',
98
+ # 'https://google.com',
99
+ # 'https://bbc.com',
100
+ # ]) {
101
+ # await page.goto(currentURL);
102
+ # }
103
+ # await browser.close();
104
+ # })();
105
+ # ```
106
+ # @param xpath - A
107
+ # {@link https://developer.mozilla.org/en-US/docs/Web/XPath | xpath} of an
108
+ # element to wait for
109
+ # @param options - Optional waiting parameters
110
+ # @returns Promise which resolves when element specified by xpath string is
111
+ # added to DOM. Resolves to `null` if waiting for `hidden: true` and xpath is
112
+ # not found in DOM.
113
+ # @remarks
114
+ # The optional Argument `options` have properties:
115
+ #
116
+ # - `visible`: A boolean to wait for element to be present in DOM and to be
117
+ # visible, i.e. to not have `display: none` or `visibility: hidden` CSS
118
+ # properties. Defaults to `false`.
119
+ #
120
+ # - `hidden`: A boolean wait for element to not be found in the DOM or to be
121
+ # hidden, i.e. have `display: none` or `visibility: hidden` CSS properties.
122
+ # Defaults to `false`.
123
+ #
124
+ # - `timeout`: A number which is maximum time to wait for in milliseconds.
125
+ # Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
126
+ # value can be changed by using the {@link Page.setDefaultTimeout} method.
127
+ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
128
+ frame = @context.frame
129
+
130
+ secondary_world = frame.secondary_world
131
+ adopted_root = secondary_world.execution_context.adopt_element_handle(self)
132
+ param_xpath =
133
+ if xpath.start_with?('//')
134
+ ".#{xpath}"
135
+ else
136
+ xpath
137
+ end
138
+ unless param_xpath.start_with?('.//')
139
+ adopted_root.dispose
140
+ raise ArgumentError.new("Unsupported xpath expression: #{xpath}")
141
+ end
142
+ handle = secondary_world.wait_for_xpath(param_xpath, visible: visible, hidden: hidden, timeout: timeout, root: adopted_root)
143
+ adopted_root.dispose
144
+ return nil unless handle
145
+
146
+ main_world = frame.main_world
147
+ result = main_world.execution_context.adopt_element_handle(handle)
148
+ handle.dispose
149
+ result
150
+ end
151
+
152
+ define_async_method :async_wait_for_xpath
153
+
79
154
  def as_element
80
155
  self
81
156
  end
@@ -309,22 +384,35 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
309
384
  end
310
385
 
311
386
  fn = <<~JAVASCRIPT
312
- (element, values) => {
387
+ (element, vals) => {
388
+ const values = new Set(vals);
313
389
  if (element.nodeName.toLowerCase() !== 'select') {
314
390
  throw new Error('Element is not a <select> element.');
315
391
  }
316
392
 
317
- const options = Array.from(element.options);
318
- element.value = undefined;
319
- for (const option of options) {
320
- option.selected = values.includes(option.value);
321
- if (option.selected && !element.multiple) {
322
- break;
393
+ const selectedValues = new Set();
394
+ if (!element.multiple) {
395
+ for (const option of element.options) {
396
+ option.selected = false;
397
+ }
398
+ for (const option of element.options) {
399
+ if (values.has(option.value)) {
400
+ option.selected = true;
401
+ selectedValues.add(option.value);
402
+ break;
403
+ }
404
+ }
405
+ } else {
406
+ for (const option of element.options) {
407
+ option.selected = values.has(option.value);
408
+ if (option.selected) {
409
+ selectedValues.add(option.value);
410
+ }
323
411
  }
324
412
  }
325
413
  element.dispatchEvent(new Event('input', { bubbles: true }));
326
414
  element.dispatchEvent(new Event('change', { bubbles: true }));
327
- return options.filter(option => option.selected).map(option => option.value);
415
+ return [...selectedValues.values()];
328
416
  }
329
417
  JAVASCRIPT
330
418
  evaluate(fn, values)
@@ -337,10 +425,6 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
337
425
  raise ArgumentError.new('Multiple file uploads only work with <input type=file multiple>')
338
426
  end
339
427
 
340
- if error_path = file_paths.find { |file_path| !File.exist?(file_path) }
341
- raise ArgumentError.new("#{error_path} does not exist or is not readable")
342
- end
343
-
344
428
  backend_node_id = @remote_object.node_info(@client)["node"]["backendNodeId"]
345
429
 
346
430
  # The zero-length array is a special case, it seems that DOM.setFileInputFiles does
@@ -421,7 +505,15 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
421
505
  end
422
506
  end
423
507
 
424
- def screenshot(type: nil, path: nil, full_page: nil, clip: nil, quality: nil, omit_background: nil, encoding: nil)
508
+ def screenshot(type: nil,
509
+ path: nil,
510
+ full_page: nil,
511
+ clip: nil,
512
+ quality: nil,
513
+ omit_background: nil,
514
+ encoding: nil,
515
+ capture_beyond_viewport: nil,
516
+ from_surface: nil)
425
517
  needs_viewport_reset = false
426
518
 
427
519
  box = bounding_box
@@ -465,7 +557,17 @@ class Puppeteer::ElementHandle < Puppeteer::JSHandle
465
557
  }
466
558
  end
467
559
 
468
- @page.screenshot(type: type, path: path, full_page: full_page, clip: clip, quality: quality, omit_background: omit_background, encoding: encoding)
560
+ @page.screenshot(
561
+ type: type,
562
+ path: path,
563
+ full_page:
564
+ full_page,
565
+ clip: clip,
566
+ quality: quality,
567
+ omit_background: omit_background,
568
+ encoding: encoding,
569
+ capture_beyond_viewport: capture_beyond_viewport,
570
+ from_surface: from_surface)
469
571
  ensure
470
572
  if needs_viewport_reset
471
573
  @page.viewport = viewport
@@ -2,7 +2,7 @@ class Puppeteer::ExecutionContext
2
2
  include Puppeteer::IfPresent
3
3
  using Puppeteer::DefineAsyncMethod
4
4
 
5
- EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__'
5
+ EVALUATION_SCRIPT_URL = 'pprt://__puppeteer_evaluation_script__'
6
6
  SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m
7
7
 
8
8
  # @param client [Puppeteer::CDPSession]
@@ -10,6 +10,7 @@ class Puppeteer::Frame
10
10
  @parent_frame = parent_frame
11
11
  @id = frame_id
12
12
  @detached = false
13
+ @has_started_loading = false
13
14
 
14
15
  @loader_id = ''
15
16
  @lifecycle_events = Set.new
@@ -46,6 +47,10 @@ class Puppeteer::Frame
46
47
 
47
48
  attr_accessor :frame_manager, :id, :loader_id, :lifecycle_events, :main_world, :secondary_world
48
49
 
50
+ def has_started_loading?
51
+ @has_started_loading
52
+ end
53
+
49
54
  # @param url [String]
50
55
  # @param rederer [String]
51
56
  # @param timeout [number|nil]
@@ -316,6 +321,10 @@ class Puppeteer::Frame
316
321
  @lifecycle_events << name
317
322
  end
318
323
 
324
+ def handle_loading_started
325
+ @has_started_loading = true
326
+ end
327
+
319
328
  def handle_loading_stopped
320
329
  @lifecycle_events << 'DOMContentLoaded'
321
330
  @lifecycle_events << 'load'
@@ -43,6 +43,9 @@ class Puppeteer::FrameManager
43
43
  client.on_event('Page.frameDetached') do |event|
44
44
  handle_frame_detached(event['frameId'], event['reason'])
45
45
  end
46
+ client.on_event('Page.frameStartedLoading') do |event|
47
+ handle_frame_started_loading(event['frameId'])
48
+ end
46
49
  client.on_event('Page.frameStoppedLoading') do |event|
47
50
  handle_frame_stopped_loading(event['frameId'])
48
51
  end
@@ -71,11 +74,17 @@ class Puppeteer::FrameManager
71
74
  private def init(cdp_session = nil)
72
75
  client = cdp_session || @client
73
76
 
74
- results = await_all(
77
+ promises = [
75
78
  client.async_send_message('Page.enable'),
76
79
  client.async_send_message('Page.getFrameTree'),
77
- )
78
- frame_tree = results.last['frameTree']
80
+ cdp_session&.async_send_message('Target.setAutoAttach', {
81
+ autoAttach: true,
82
+ waitForDebuggerOnStart: false,
83
+ flatten: true,
84
+ }),
85
+ ].compact
86
+ results = await_all(*promises)
87
+ frame_tree = results[1]['frameTree']
79
88
  handle_frame_tree(client, frame_tree)
80
89
  await_all(
81
90
  client.async_send_message('Page.setLifecycleEventsEnabled', enabled: true),
@@ -112,13 +121,12 @@ class Puppeteer::FrameManager
112
121
  option_timeout = timeout || @timeout_settings.navigation_timeout
113
122
 
114
123
  watcher = Puppeteer::LifecycleWatcher.new(self, frame, option_wait_until, option_timeout)
115
- ensure_new_document_navigation = false
116
124
 
117
125
  begin
118
126
  navigate = future do
119
127
  result = @client.send_message('Page.navigate', navigate_params)
120
128
  loader_id = result['loaderId']
121
- ensure_new_document_navigation = !!loader_id
129
+
122
130
  if result['errorText']
123
131
  raise NavigationError.new("#{result['errorText']} at #{url}")
124
132
  end
@@ -128,14 +136,9 @@ class Puppeteer::FrameManager
128
136
  watcher.timeout_or_termination_promise,
129
137
  )
130
138
 
131
- document_navigation_promise =
132
- if ensure_new_document_navigation
133
- watcher.new_document_navigation_promise
134
- else
135
- watcher.same_document_navigation_promise
136
- end
137
139
  await_any(
138
- document_navigation_promise,
140
+ watcher.new_document_navigation_promise,
141
+ watcher.same_document_navigation_promise,
139
142
  watcher.timeout_or_termination_promise,
140
143
  )
141
144
  rescue Puppeteer::TimeoutError => err
@@ -201,7 +204,14 @@ class Puppeteer::FrameManager
201
204
  emit_event(FrameManagerEmittedEvents::LifecycleEvent, frame)
202
205
  end
203
206
 
204
- # @param {string} frameId
207
+ # @param frame_id [String]
208
+ def handle_frame_started_loading(frame_id)
209
+ frame = @frames[frame_id]
210
+ return if !frame
211
+ frame.handle_loading_started
212
+ end
213
+
214
+ # @param frame_id [String]
205
215
  def handle_frame_stopped_loading(frame_id)
206
216
  frame = @frames[frame_id]
207
217
  return if !frame
@@ -31,9 +31,20 @@ module Puppeteer::Launcher
31
31
  # `default_viewport: nil` must be respected here.
32
32
  @default_viewport = options.key?(:default_viewport) ? options[:default_viewport] : Puppeteer::Viewport.new(width: 800, height: 600)
33
33
  @slow_mo = options[:slow_mo] || 0
34
+
35
+ # only for Puppeteer.connect
36
+ @target_filter = options[:target_filter]
37
+ if @target_filter && !@target_filter.respond_to?(:call)
38
+ raise ArgumentError.new('target_filter must be a Proc (target_info => true/false)')
39
+ end
40
+
41
+ @is_page_target = options[:is_page_target]
42
+ if @is_page_target && !@is_page_target.respond_to?(:call)
43
+ raise ArgumentError.new('is_page_target must be a Proc (target_info => true/false)')
44
+ end
34
45
  end
35
46
 
36
- attr_reader :default_viewport, :slow_mo
47
+ attr_reader :default_viewport, :slow_mo, :target_filter, :is_page_target
37
48
 
38
49
  def ignore_https_errors?
39
50
  @ignore_https_errors
@@ -87,6 +87,8 @@ module Puppeteer::Launcher
87
87
  default_viewport: @browser_options.default_viewport,
88
88
  process: runner.proc,
89
89
  close_callback: -> { runner.close },
90
+ target_filter_callback: nil,
91
+ is_page_target_callback: nil,
90
92
  )
91
93
  rescue
92
94
  runner.kill
@@ -122,7 +124,9 @@ module Puppeteer::Launcher
122
124
  '--disable-default-apps',
123
125
  '--disable-dev-shm-usage',
124
126
  '--disable-extensions',
125
- '--disable-features=Translate,BackForwardCache',
127
+ # TODO: remove AvoidUnnecessaryBeforeUnloadCheckSync below
128
+ # once crbug.com/1324138 is fixed and released.
129
+ '--disable-features=Translate,BackForwardCache,AvoidUnnecessaryBeforeUnloadCheckSync',
126
130
  '--disable-hang-monitor',
127
131
  '--disable-ipc-flooding-protection',
128
132
  '--disable-popup-blocking',
@@ -205,6 +209,8 @@ module Puppeteer::Launcher
205
209
  default_viewport: @browser_options.default_viewport,
206
210
  process: nil,
207
211
  close_callback: -> { connection.send_message('Browser.close') },
212
+ target_filter_callback: @browser_options.target_filter,
213
+ is_page_target_callback: @browser_options.is_page_target,
208
214
  )
209
215
  end
210
216
 
@@ -85,6 +85,8 @@ module Puppeteer::Launcher
85
85
  default_viewport: @browser_options.default_viewport,
86
86
  process: runner.proc,
87
87
  close_callback: -> { runner.close },
88
+ target_filter_callback: nil,
89
+ is_page_target_callback: nil,
88
90
  )
89
91
  rescue
90
92
  runner.kill
@@ -132,6 +134,8 @@ module Puppeteer::Launcher
132
134
  default_viewport: @browser_options.default_viewport,
133
135
  process: nil,
134
136
  close_callback: -> { connection.send_message('Browser.close') },
137
+ target_filter_callback: nil,
138
+ is_page_target_callback: nil,
135
139
  )
136
140
  end
137
141
 
@@ -43,7 +43,7 @@ class Puppeteer::LifecycleWatcher
43
43
  if expected_lifecycle.any? { |event| !frame.lifecycle_events.include?(event) }
44
44
  return false
45
45
  end
46
- if frame.child_frames.any? { |child| !completed?(child) }
46
+ if frame.child_frames.any? { |child| child.has_started_loading? && !completed?(child) }
47
47
  return false
48
48
  end
49
49
  true
@@ -65,7 +65,6 @@ class Puppeteer::LifecycleWatcher
65
65
  @expected_lifecycle = ExpectedLifecycle.new(wait_until)
66
66
  @frame_manager = frame_manager
67
67
  @frame = frame
68
- @initial_loader_id = frame.loader_id
69
68
  @timeout = timeout
70
69
 
71
70
  @listener_ids = {}
@@ -77,6 +76,7 @@ class Puppeteer::LifecycleWatcher
77
76
  check_lifecycle_complete
78
77
  end,
79
78
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameNavigatedWithinDocument, &method(:navigated_within_document)),
79
+ @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameNavigated, &method(:navigated)),
80
80
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameSwapped, &method(:handle_frame_swapped)),
81
81
  @frame_manager.add_event_listener(FrameManagerEmittedEvents::FrameDetached, &method(:handle_frame_detached)),
82
82
  ]
@@ -143,6 +143,12 @@ class Puppeteer::LifecycleWatcher
143
143
  check_lifecycle_complete
144
144
  end
145
145
 
146
+ private def navigated(frame)
147
+ return if frame != @frame
148
+ @new_document_navigation = true
149
+ check_lifecycle_complete
150
+ end
151
+
146
152
  private def handle_frame_swapped(frame)
147
153
  return if frame != @frame
148
154
  @swapped = true
@@ -153,17 +159,10 @@ class Puppeteer::LifecycleWatcher
153
159
  # We expect navigation to commit.
154
160
  return unless @expected_lifecycle.completed?(@frame)
155
161
  @lifecycle_promise.fulfill(true) if @lifecycle_promise.pending?
156
- if @frame.loader_id == @initial_loader_id && !@has_same_document_navigation
157
- if @swapped
158
- @swapped = false
159
- @new_document_navigation_promise.fulfill(true)
160
- end
161
- return
162
- end
163
162
  if @has_same_document_navigation && @same_document_navigation_promise.pending?
164
163
  @same_document_navigation_promise.fulfill(true)
165
164
  end
166
- if @frame.loader_id != @initial_loader_id && @new_document_navigation_promise.pending?
165
+ if (@swapped || @new_document_navigation) && @new_document_navigation_promise.pending?
167
166
  @new_document_navigation_promise.fulfill(true)
168
167
  end
169
168
  end
@@ -2,15 +2,49 @@ require 'mime/types'
2
2
 
3
3
  class Puppeteer::Page
4
4
  # /**
5
- # * @typedef {Object} ScreenshotOptions
6
- # * @property {string=} type
7
- # * @property {string=} path
8
- # * @property {boolean=} fullPage
9
- # * @property {{x: number, y: number, width: number, height: number}=} clip
10
- # * @property {number=} quality
11
- # * @property {boolean=} omitBackground
12
- # * @property {string=} encoding
13
- # */
5
+ # * @defaultValue 'png'
6
+ # */
7
+ # type?: 'png' | 'jpeg' | 'webp';
8
+ # /**
9
+ # * The file path to save the image to. The screenshot type will be inferred
10
+ # * from file extension. If path is a relative path, then it is resolved
11
+ # * relative to current working directory. If no path is provided, the image
12
+ # * won't be saved to the disk.
13
+ # */
14
+ # path?: string;
15
+ # /**
16
+ # * When true, takes a screenshot of the full page.
17
+ # * @defaultValue false
18
+ # */
19
+ # fullPage?: boolean;
20
+ # /**
21
+ # * An object which specifies the clipping region of the page.
22
+ # */
23
+ # clip?: ScreenshotClip;
24
+ # /**
25
+ # * Quality of the image, between 0-100. Not applicable to `png` images.
26
+ # */
27
+ # quality?: number;
28
+ # /**
29
+ # * Hides default white background and allows capturing screenshots with transparency.
30
+ # * @defaultValue false
31
+ # */
32
+ # omitBackground?: boolean;
33
+ # /**
34
+ # * Encoding of the image.
35
+ # * @defaultValue 'binary'
36
+ # */
37
+ # encoding?: 'base64' | 'binary';
38
+ # /**
39
+ # * Capture the screenshot beyond the viewport.
40
+ # * @defaultValue true
41
+ # */
42
+ # captureBeyondViewport?: boolean;
43
+ # /**
44
+ # * Capture the screenshot from the surface, rather than the view.
45
+ # * @defaultValue true
46
+ # */
47
+ # fromSurface?: boolean;
14
48
  class ScreenshotOptions
15
49
  # @params options [Hash]
16
50
  def initialize(options)
@@ -65,6 +99,18 @@ class Puppeteer::Page
65
99
  @clip = options[:clip]
66
100
  @omit_background = options[:omit_background]
67
101
  @encoding = options[:encoding]
102
+ @capture_beyond_viewport =
103
+ if options[:capture_beyond_viewport].nil?
104
+ true
105
+ else
106
+ options[:capture_beyond_viewport]
107
+ end
108
+ @from_surface =
109
+ if options[:from_surface].nil?
110
+ true
111
+ else
112
+ options[:from_surface]
113
+ end
68
114
  end
69
115
 
70
116
  attr_reader :type, :quality, :path, :clip, :encoding
@@ -76,5 +122,13 @@ class Puppeteer::Page
76
122
  def omit_background?
77
123
  @omit_background
78
124
  end
125
+
126
+ def capture_beyond_viewport?
127
+ @capture_beyond_viewport
128
+ end
129
+
130
+ def from_surface?
131
+ @from_surface
132
+ end
79
133
  end
80
134
  end
@@ -814,6 +814,10 @@ class Puppeteer::Page
814
814
  predicate
815
815
  end
816
816
 
817
+ frames.each do |frame|
818
+ return frame if frame_predicate.call(frame)
819
+ end
820
+
817
821
  wait_for_frame_manager_event(
818
822
  FrameManagerEmittedEvents::FrameAttached,
819
823
  FrameManagerEmittedEvents::FrameNavigated,
@@ -1021,7 +1025,15 @@ class Puppeteer::Page
1021
1025
  # @param quality [Integer]
1022
1026
  # @param omit_background [Boolean]
1023
1027
  # @param encoding [String]
1024
- def screenshot(type: nil, path: nil, full_page: nil, clip: nil, quality: nil, omit_background: nil, encoding: nil)
1028
+ def screenshot(type: nil,
1029
+ path: nil,
1030
+ full_page: nil,
1031
+ clip: nil,
1032
+ quality: nil,
1033
+ omit_background: nil,
1034
+ encoding: nil,
1035
+ capture_beyond_viewport: nil,
1036
+ from_surface: nil)
1025
1037
  options = {
1026
1038
  type: type,
1027
1039
  path: path,
@@ -1030,6 +1042,8 @@ class Puppeteer::Page
1030
1042
  quality: quality,
1031
1043
  omit_background: omit_background,
1032
1044
  encoding: encoding,
1045
+ capture_beyond_viewport: capture_beyond_viewport,
1046
+ from_surface: from_surface,
1033
1047
  }.compact
1034
1048
  screenshot_options = ScreenshotOptions.new(options)
1035
1049
 
@@ -1058,18 +1072,20 @@ class Puppeteer::Page
1058
1072
  # Overwrite clip for full page at all times.
1059
1073
  clip = { x: 0, y: 0, width: width, height: height, scale: 1 }
1060
1074
 
1061
- screen_orientation =
1062
- if @viewport&.landscape?
1063
- { angle: 90, type: 'landscapePrimary' }
1064
- else
1065
- { angle: 0, type: 'portraitPrimary' }
1066
- end
1067
- @client.send_message('Emulation.setDeviceMetricsOverride',
1068
- mobile: @viewport&.mobile? || false,
1069
- width: width,
1070
- height: height,
1071
- deviceScaleFactor: @viewport&.device_scale_factor || 1,
1072
- screenOrientation: screen_orientation)
1075
+ unless screenshot_options.capture_beyond_viewport?
1076
+ screen_orientation =
1077
+ if @viewport&.landscape?
1078
+ { angle: 90, type: 'landscapePrimary' }
1079
+ else
1080
+ { angle: 0, type: 'portraitPrimary' }
1081
+ end
1082
+ @client.send_message('Emulation.setDeviceMetricsOverride',
1083
+ mobile: @viewport&.mobile? || false,
1084
+ width: width,
1085
+ height: height,
1086
+ deviceScaleFactor: @viewport&.device_scale_factor || 1,
1087
+ screenOrientation: screen_orientation)
1088
+ end
1073
1089
  end
1074
1090
 
1075
1091
  should_set_default_background = screenshot_options.omit_background? && format == 'png'
@@ -1078,6 +1094,8 @@ class Puppeteer::Page
1078
1094
  format: format,
1079
1095
  quality: screenshot_options.quality,
1080
1096
  clip: clip,
1097
+ captureBeyondViewport: screenshot_options.capture_beyond_viewport?,
1098
+ fromSurface: screenshot_options.from_surface?,
1081
1099
  }.compact
1082
1100
  result = @client.send_message('Page.captureScreenshot', screenshot_params)
1083
1101
  reset_default_background_color if should_set_default_background
@@ -18,14 +18,19 @@ class Puppeteer::Target
18
18
  # @param {!function():!Promise<!Puppeteer.CDPSession>} sessionFactory
19
19
  # @param {boolean} ignoreHTTPSErrors
20
20
  # @param {?Puppeteer.Viewport} defaultViewport
21
- def initialize(target_info:, browser_context:, session_factory:, ignore_https_errors:, default_viewport:)
21
+ def initialize(target_info:,
22
+ browser_context:,
23
+ session_factory:,
24
+ ignore_https_errors:,
25
+ default_viewport:,
26
+ is_page_target_callback:)
22
27
  @target_info = target_info
23
28
  @browser_context = browser_context
24
29
  @target_id = target_info.target_id
25
30
  @session_factory = session_factory
26
31
  @ignore_https_errors = ignore_https_errors
27
32
  @default_viewport = default_viewport
28
-
33
+ @is_page_target_callback = is_page_target_callback
29
34
 
30
35
  # /** @type {?Promise<!Puppeteer.Page>} */
31
36
  # this._pagePromise = null;
@@ -37,14 +42,14 @@ class Puppeteer::Target
37
42
  end
38
43
  @is_closed_promise = resolvable_future
39
44
 
40
- @is_initialized = @target_info.type != 'page' || !@target_info.url.empty?
45
+ @is_initialized = !@is_page_target_callback.call(@target_info) || !@target_info.url.empty?
41
46
 
42
47
  if @is_initialized
43
48
  @initialize_callback_promise.fulfill(true)
44
49
  end
45
50
  end
46
51
 
47
- attr_reader :target_id, :initialized_promise, :is_closed_promise
52
+ attr_reader :target_id, :target_info, :initialized_promise, :is_closed_promise
48
53
 
49
54
  def closed_callback
50
55
  @is_closed_promise.fulfill(true)
@@ -83,7 +88,7 @@ class Puppeteer::Target
83
88
  end
84
89
 
85
90
  def page
86
- if ['page', 'background_page', 'webview'].include?(@target_info.type) && @page.nil?
91
+ if @is_page_target_callback.call(@target_info) && @page.nil?
87
92
  client = @session_factory.call
88
93
  @page = Puppeteer::Page.create(client, self, @ignore_https_errors, @default_viewport)
89
94
  end
@@ -145,7 +150,7 @@ class Puppeteer::Target
145
150
  def handle_target_info_changed(target_info)
146
151
  @target_info = target_info
147
152
 
148
- if !@is_initialized && (@target_info.type != 'page' || !@target_info.url.empty?)
153
+ if !@is_initialized && (!@is_page_target_callback.call(@target_info) || !@target_info.url.empty?)
149
154
  @is_initialized = true
150
155
  @initialize_callback_promise.fulfill(true)
151
156
  end
@@ -1,3 +1,3 @@
1
1
  module Puppeteer
2
- VERSION = '0.40.7'
2
+ VERSION = '0.41.0'
3
3
  end
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency 'rollbar'
33
33
  spec.add_development_dependency 'rspec', '~> 3.11.0'
34
34
  spec.add_development_dependency 'rspec_junit_formatter' # for CircleCI.
35
- spec.add_development_dependency 'rubocop', '~> 1.29.0'
35
+ spec.add_development_dependency 'rubocop', '~> 1.31.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.40.7
4
+ version: 0.41.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-05-10 00:00:00.000000000 Z
11
+ date: 2022-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -170,14 +170,14 @@ dependencies:
170
170
  requirements:
171
171
  - - "~>"
172
172
  - !ruby/object:Gem::Version
173
- version: 1.29.0
173
+ version: 1.31.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.29.0
180
+ version: 1.31.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: rubocop-rspec
183
183
  requirement: !ruby/object:Gem::Requirement