puppeteer-ruby 0.40.5 → 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: 2f6c1abfbf66f645ac208cf5f3f0ad24c4ae421e576c5b1516803cdc897bad22
4
- data.tar.gz: a40b422147eff9261720975d8009ecc3fbd24a1f0d2d828f1fe3b5dce31f5c24
3
+ metadata.gz: ccfb160c40e2ef032c0da17ab4b1cc9fe610d4caf42f236042ea0757c9e270e7
4
+ data.tar.gz: 95a3a457aa850116bc3062e9507ef53bd10e1d451ebeb4703cf9a6f5d663b9c5
5
5
  SHA512:
6
- metadata.gz: f532e2ff58c283c0904d5c76c0e9254d5b91be1f6ff7c576b728d91a727b0bb246f9d0eefe1461b1a7ba28607f33ac7a541d9d9a9ee3da0e5742c330f3c01b6e
7
- data.tar.gz: c0dab5bca1e3ae23c6c2f3866cb3127024a9fd9663e01ff20d7edd68ef60060fc2203ffe81d80f322a10428ea4632193e1eef8b502e3375ea0c8413a3172dc61
6
+ metadata.gz: b65da9f89cc8cfeea05e4e0835f919aab382f47e380d27c1e0a439c70cb0b0b7d9e8519a3361ad545e0c5115216d698740e46f20c96960cf96496ee22ac374ea
7
+ data.tar.gz: 81c846c795efd81bc10af84b65e02a6dfc3a52d9be2cc5f21cc7bc313e84fcd4ef3fed692bc1bc616389bc43f2a06a6944c2353aa8f8c3a614bb8a4c8ef2097c
data/CHANGELOG.md CHANGED
@@ -1,7 +1,19 @@
1
- ### main [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.5...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
+
9
+ ### 0.40.7 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.6...0.40.7)]
10
+
11
+ - Port Puppeteer v13.6-v13.7 features.
12
+
13
+ ### 0.40.6 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.5...0.40.6)]
14
+
15
+ - Port Puppeteer v13.1-v13.5 features mainly for request interception.
16
+
5
17
  ### 0.40.5 [[diff](https://github.com/YusukeIwaki/puppeteer-ruby/compare/0.40.4...0.40.5)]
6
18
 
7
19
  Bugfix:
data/README.md CHANGED
@@ -229,6 +229,13 @@ 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
  ## Contributing
233
240
 
234
241
  Bug reports and pull requests are welcome on GitHub at https://github.com/YusukeIwaki/puppeteer-ruby.
data/docs/api_coverage.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # API coverages
2
- - Puppeteer version: v13.5.1
3
- - puppeteer-ruby version: 0.40.4
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
 
@@ -310,8 +311,8 @@
310
311
  * frame
311
312
  * headers
312
313
  * initiator
313
- * ~~interceptResolutionState~~
314
- * ~~isInterceptResolutionHandled~~
314
+ * interceptResolutionState => `#intercept_resolution_state`
315
+ * isInterceptResolutionHandled => `#intercept_resolution_handled?`
315
316
  * isNavigationRequest => `#navigation_request?`
316
317
  * method
317
318
  * postData => `#post_data`
@@ -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?
@@ -128,7 +128,7 @@ class Puppeteer::BrowserRunner
128
128
  def close
129
129
  return if @closed
130
130
 
131
- if @using_temp_user_data_dir && !@for_firefox
131
+ if @using_temp_user_data_dir
132
132
  kill
133
133
  elsif @connection
134
134
  begin
@@ -191,6 +191,7 @@ class Puppeteer::Connection
191
191
  'Network.responseReceived',
192
192
  'Network.responseReceivedExtraInfo',
193
193
  'Page.lifecycleEvent',
194
+ 'Target.receivedMessageFromTarget', # only Firefox
194
195
  ]
195
196
 
196
197
  def handle_message(message)
@@ -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
@@ -2,6 +2,8 @@ class Puppeteer::HTTPRequest
2
2
  include Puppeteer::DebugPrint
3
3
  include Puppeteer::IfPresent
4
4
 
5
+ DEFAULT_INTERCEPT_RESOLUTION_PRIORITY = 0
6
+
5
7
  # defines some methods used only in NetworkManager, Response
6
8
  class InternalAccessor
7
9
  def initialize(request)
@@ -38,6 +40,43 @@ class Puppeteer::HTTPRequest
38
40
  end
39
41
  end
40
42
 
43
+ class InterceptResolutionState
44
+ def self.abort(priority: nil)
45
+ new(action: 'abort', priority: priority)
46
+ end
47
+
48
+ def self.respond(priority: nil)
49
+ new(action: 'respond', priority: priority)
50
+ end
51
+
52
+ def self.continue(priority: nil)
53
+ new(action: 'continue', priority: priority)
54
+ end
55
+
56
+ def self.disabled(priority: nil)
57
+ new(action: 'disabled', priority: priority)
58
+ end
59
+
60
+ def self.none(priority: nil)
61
+ new(action: 'none', priority: priority)
62
+ end
63
+
64
+ def self.already_handled(priority: nil)
65
+ new(action: 'already-handled', priority: priority)
66
+ end
67
+
68
+ private def initialize(action:, priority:)
69
+ @action = action
70
+ @priority = priority
71
+ end
72
+
73
+ def priority_unspecified?
74
+ @priority.nil?
75
+ end
76
+
77
+ attr_reader :action, :priority
78
+ end
79
+
41
80
  # @param client [Puppeteer::CDPSession]
42
81
  # @param frame [Puppeteer::Frame]
43
82
  # @param interception_id [string|nil]
@@ -57,9 +96,8 @@ class Puppeteer::HTTPRequest
57
96
  @frame = frame
58
97
  @redirect_chain = redirect_chain
59
98
  @continue_request_overrides = {}
60
- @current_strategy = 'none'
61
- @current_priority = nil
62
- @intercept_actions = []
99
+ @intercept_resolution_state = InterceptResolutionState.none
100
+ @intercept_handlers = []
63
101
  @initiator = event['initiator']
64
102
 
65
103
  @headers = {}
@@ -115,19 +153,29 @@ class Puppeteer::HTTPRequest
115
153
  @abort_error_reason
116
154
  end
117
155
 
118
- # @returns An array of the current intercept resolution strategy and priority
119
- # `[strategy,priority]`. Strategy is one of: `abort`, `respond`, `continue`,
120
- # `disabled`, `none`, or `already-handled`.
121
- def intercept_resolution
156
+ # @returns An InterceptResolutionState object describing the current resolution
157
+ # action and priority.
158
+ #
159
+ # InterceptResolutionState contains:
160
+ # action: InterceptResolutionAction
161
+ # priority?: number
162
+ #
163
+ # InterceptResolutionAction is one of: `abort`, `respond`, `continue`,
164
+ # `disabled`, `none`, or `alreay-handled`
165
+ def intercept_resolution_state
122
166
  if !@allow_interception
123
- ['disabled']
167
+ InterceptResolutionState.disabled
124
168
  elsif @interception_handled
125
- ['already-handled']
169
+ InterceptResolutionState.already_handled
126
170
  else
127
- [@current_strategy, @current_priority]
171
+ @intercept_resolution_state.dup
128
172
  end
129
173
  end
130
174
 
175
+ def intercept_resolution_handled?
176
+ @interception_handled
177
+ end
178
+
131
179
  # Adds an async request handler to the processing queue.
132
180
  # Deferred handlers are not guaranteed to execute in any particular order,
133
181
  # but they are guarnateed to resolve before the request interception
@@ -135,19 +183,19 @@ class Puppeteer::HTTPRequest
135
183
  #
136
184
  # @param pending_handler [Proc]
137
185
  def enqueue_intercept_action(pending_handler)
138
- @intercept_actions << pending_handler
186
+ @intercept_handlers << pending_handler
139
187
  end
140
188
 
141
189
  # Awaits pending interception handlers and then decides how to fulfill
142
190
  # the request interception.
143
191
  def finalize_interceptions
144
- @intercept_actions.each(&:call)
145
- case @intercept_resolution
146
- when :abort
192
+ @intercept_handlers.each(&:call)
193
+ case intercept_resolution_state.action
194
+ when 'abort'
147
195
  abort_impl(**@abort_error_reason)
148
- when :respond
196
+ when 'respond'
149
197
  respond_impl(**@response_for_request)
150
- when :continue
198
+ when 'continue'
151
199
  continue_impl(@continue_request_overrides)
152
200
  end
153
201
  end
@@ -169,8 +217,14 @@ class Puppeteer::HTTPRequest
169
217
  private def headers_to_array(headers)
170
218
  return nil unless headers
171
219
 
172
- headers.map do |key, value|
173
- { name: key, value: value.to_s }
220
+ headers.flat_map do |key, value|
221
+ if value.is_a?(Enumerable)
222
+ value.map do |v|
223
+ { name: key, value: v.to_s }
224
+ end
225
+ else
226
+ { name: key, value: value.to_s }
227
+ end
174
228
  end
175
229
  end
176
230
 
@@ -220,17 +274,16 @@ class Puppeteer::HTTPRequest
220
274
  end
221
275
 
222
276
  @continue_request_overrides = overrides
223
- if @current_priority.nil? || priority > @current_priority
224
- @current_strategy = :continue
225
- @current_priority = priority
277
+ if @intercept_resolution_state.priority_unspecified? || priority > @intercept_resolution_state.priority
278
+ @intercept_resolution_state = InterceptResolutionState.continue(priority: priority)
226
279
  return
227
280
  end
228
281
 
229
- if priority == @current_priority
230
- if @current_strategy == :abort || @current_strategy == :respond
282
+ if priority == @intercept_resolution_state.priority
283
+ if @intercept_resolution_state.action == :abort || @intercept_resolution_state.action == :respond
231
284
  return
232
285
  end
233
- @current_strategy = :continue
286
+ @intercept_resolution_state = InterceptResolutionState.continue(priority: priority)
234
287
  end
235
288
  end
236
289
 
@@ -284,17 +337,16 @@ class Puppeteer::HTTPRequest
284
337
  body: body,
285
338
  }
286
339
 
287
- if @current_priority.nil? || priority > @current_priority
288
- @current_strategy = :respond
289
- @current_priority = priority
340
+ if @intercept_resolution_state.priority_unspecified? || priority > @intercept_resolution_state.priority
341
+ @intercept_resolution_state = InterceptResolutionState.respond(priority: priority)
290
342
  return
291
343
  end
292
344
 
293
- if priority == @current_priority
294
- if @current_strategy == :abort
345
+ if priority == @intercept_resolution_state.priority
346
+ if @intercept_resolution_state.action == :abort
295
347
  return
296
348
  end
297
- @current_strategy = :respond
349
+ @intercept_resolution_state = InterceptResolutionState.respond(priority: priority)
298
350
  end
299
351
  end
300
352
 
@@ -303,7 +355,12 @@ class Puppeteer::HTTPRequest
303
355
 
304
356
  mock_response_headers = {}
305
357
  headers&.each do |key, value|
306
- mock_response_headers[key.downcase] = value
358
+ mock_response_headers[key.downcase] =
359
+ if value.is_a?(Enumerable)
360
+ value.map(&:to_s)
361
+ else
362
+ value.to_s
363
+ end
307
364
  end
308
365
  if content_type
309
366
  mock_response_headers['content-type'] = content_type
@@ -360,9 +417,8 @@ class Puppeteer::HTTPRequest
360
417
  end
361
418
  @abort_error_reason = error_reason
362
419
 
363
- if @current_priority.nil? || priority > @current_priority
364
- @current_strategy = :abort
365
- @current_priority = priority
420
+ if @intercept_resolution_state.priority_unspecified? || priority > @intercept_resolution_state.priority
421
+ @intercept_resolution_state = InterceptResolutionState.abort(priority: priority)
366
422
  end
367
423
  end
368
424
 
@@ -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',
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
@@ -119,4 +119,8 @@ class Puppeteer::NetworkEventManager
119
119
  def get_queued_event_group(network_request_id)
120
120
  @queued_event_group_map[network_request_id]
121
121
  end
122
+
123
+ def forget_queued_event_group(network_request_id)
124
+ @queued_event_group_map.delete(network_request_id)
125
+ end
122
126
  end
@@ -217,6 +217,7 @@ class Puppeteer::NetworkManager
217
217
  # CDP may have sent a Fetch.requestPaused event already. Check for it.
218
218
  if_present(@network_event_manager.get_request_paused(network_request_id)) do |request_paused_event|
219
219
  fetch_request_id = request_paused_event['requestId']
220
+ patch_request_event_headers(event, request_paused_event)
220
221
  handle_request(event, fetch_request_id)
221
222
  @network_event_manager.forget_request_paused(network_request_id)
222
223
  end
@@ -277,12 +278,19 @@ class Puppeteer::NetworkManager
277
278
  end
278
279
 
279
280
  if request_will_be_sent_event
281
+ patch_request_event_headers(request_will_be_sent_event, event)
280
282
  handle_request(request_will_be_sent_event, fetch_request_id)
281
283
  else
282
284
  @network_event_manager.store_request_paused(network_request_id, event)
283
285
  end
284
286
  end
285
287
 
288
+ private def patch_request_event_headers(request_will_be_sent_event, request_paused_event)
289
+ request_will_be_sent_event['request']['headers'].merge!(
290
+ # includes extra headers, like: Accept, Origin
291
+ request_paused_event['request']['headers'])
292
+ end
293
+
286
294
  private def handle_request(event, fetch_request_id)
287
295
  redirect_chain = []
288
296
  if event['redirectResponse']
@@ -387,6 +395,7 @@ class Puppeteer::NetworkManager
387
395
  # We may have skipped response and loading events because we didn't have
388
396
  # this ExtraInfo event yet. If so, emit those events now.
389
397
  if_present(@network_event_manager.get_queued_event_group(event['requestId'])) do |queued_events|
398
+ @network_event_manager.forget_queued_event_group(event['requestId'])
390
399
  emit_response_event(queued_events.response_received_event, event)
391
400
  if_present(queued_events.loading_finished_event) do |loading_finished_event|
392
401
  emit_loading_finished(loading_finished_event)
@@ -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
@@ -389,7 +389,18 @@ class Puppeteer::Page
389
389
  @client.send_message('Network.getCookies', urls: (urls.empty? ? [url] : urls))['cookies']
390
390
  end
391
391
 
392
+ # check if each cookie element has required fields ('name' and 'value')
393
+ private def assert_cookie_params(cookies, requires:)
394
+ return if cookies.all? do |cookie|
395
+ requires.all? { |field_name| cookie[field_name] || cookie[field_name.to_s] }
396
+ end
397
+
398
+ raise ArgumentError.new("Each coookie must have #{requires.join(" and ")} attribute.")
399
+ end
400
+
392
401
  def delete_cookie(*cookies)
402
+ assert_cookie_params(cookies, requires: %i(name))
403
+
393
404
  page_url = url
394
405
  starts_with_http = page_url.start_with?("http")
395
406
  cookies.each do |cookie|
@@ -399,6 +410,8 @@ class Puppeteer::Page
399
410
  end
400
411
 
401
412
  def set_cookie(*cookies)
413
+ assert_cookie_params(cookies, requires: %i(name value))
414
+
402
415
  page_url = url
403
416
  starts_with_http = page_url.start_with?("http")
404
417
  items = cookies.map do |cookie|
@@ -801,6 +814,10 @@ class Puppeteer::Page
801
814
  predicate
802
815
  end
803
816
 
817
+ frames.each do |frame|
818
+ return frame if frame_predicate.call(frame)
819
+ end
820
+
804
821
  wait_for_frame_manager_event(
805
822
  FrameManagerEmittedEvents::FrameAttached,
806
823
  FrameManagerEmittedEvents::FrameNavigated,
@@ -1008,7 +1025,15 @@ class Puppeteer::Page
1008
1025
  # @param quality [Integer]
1009
1026
  # @param omit_background [Boolean]
1010
1027
  # @param encoding [String]
1011
- 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)
1012
1037
  options = {
1013
1038
  type: type,
1014
1039
  path: path,
@@ -1017,6 +1042,8 @@ class Puppeteer::Page
1017
1042
  quality: quality,
1018
1043
  omit_background: omit_background,
1019
1044
  encoding: encoding,
1045
+ capture_beyond_viewport: capture_beyond_viewport,
1046
+ from_surface: from_surface,
1020
1047
  }.compact
1021
1048
  screenshot_options = ScreenshotOptions.new(options)
1022
1049
 
@@ -1045,18 +1072,20 @@ class Puppeteer::Page
1045
1072
  # Overwrite clip for full page at all times.
1046
1073
  clip = { x: 0, y: 0, width: width, height: height, scale: 1 }
1047
1074
 
1048
- screen_orientation =
1049
- if @viewport&.landscape?
1050
- { angle: 90, type: 'landscapePrimary' }
1051
- else
1052
- { angle: 0, type: 'portraitPrimary' }
1053
- end
1054
- @client.send_message('Emulation.setDeviceMetricsOverride',
1055
- mobile: @viewport&.mobile? || false,
1056
- width: width,
1057
- height: height,
1058
- deviceScaleFactor: @viewport&.device_scale_factor || 1,
1059
- 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
1060
1089
  end
1061
1090
 
1062
1091
  should_set_default_background = screenshot_options.omit_background? && format == 'png'
@@ -1065,6 +1094,8 @@ class Puppeteer::Page
1065
1094
  format: format,
1066
1095
  quality: screenshot_options.quality,
1067
1096
  clip: clip,
1097
+ captureBeyondViewport: screenshot_options.capture_beyond_viewport?,
1098
+ fromSurface: screenshot_options.from_surface?,
1068
1099
  }.compact
1069
1100
  result = @client.send_message('Page.captureScreenshot', screenshot_params)
1070
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.5'
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.26.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.5
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-03-26 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.26.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.26.0
180
+ version: 1.31.0
181
181
  - !ruby/object:Gem::Dependency
182
182
  name: rubocop-rspec
183
183
  requirement: !ruby/object:Gem::Requirement