puppeteer-ruby 0.40.5 → 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: 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