puppeteer-ruby 0.40.7 → 0.41.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 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