playwright-ruby-client 1.22.0 → 1.25.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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/accessibility.md +8 -0
  3. data/documentation/docs/api/api_request_context.md +8 -8
  4. data/documentation/docs/api/browser.md +30 -5
  5. data/documentation/docs/api/browser_context.md +20 -7
  6. data/documentation/docs/api/browser_type.md +4 -0
  7. data/documentation/docs/api/download.md +1 -1
  8. data/documentation/docs/api/element_handle.md +1 -6
  9. data/documentation/docs/api/experimental/android_device.md +4 -0
  10. data/documentation/docs/api/file_chooser.md +1 -1
  11. data/documentation/docs/api/locator.md +15 -7
  12. data/documentation/docs/api/page.md +21 -4
  13. data/documentation/docs/api/request.md +3 -1
  14. data/documentation/docs/api/response.md +12 -1
  15. data/documentation/docs/api/route.md +65 -0
  16. data/documentation/docs/api/selectors.md +2 -2
  17. data/documentation/docs/api/tracing.md +1 -1
  18. data/documentation/docs/include/api_coverage.md +5 -3
  19. data/documentation/package.json +4 -4
  20. data/documentation/yarn.lock +1876 -1304
  21. data/lib/playwright/channel.rb +1 -3
  22. data/lib/playwright/channel_owner.rb +13 -1
  23. data/lib/playwright/channel_owners/browser.rb +13 -0
  24. data/lib/playwright/channel_owners/browser_context.rb +82 -14
  25. data/lib/playwright/channel_owners/browser_type.rb +4 -0
  26. data/lib/playwright/channel_owners/frame.rb +16 -2
  27. data/lib/playwright/channel_owners/local_utils.rb +29 -0
  28. data/lib/playwright/channel_owners/page.rb +43 -15
  29. data/lib/playwright/channel_owners/request.rb +31 -6
  30. data/lib/playwright/channel_owners/response.rb +6 -0
  31. data/lib/playwright/channel_owners/route.rb +104 -45
  32. data/lib/playwright/connection.rb +20 -9
  33. data/lib/playwright/har_router.rb +82 -0
  34. data/lib/playwright/http_headers.rb +1 -1
  35. data/lib/playwright/javascript/regex.rb +23 -0
  36. data/lib/playwright/javascript/value_parser.rb +4 -0
  37. data/lib/playwright/javascript/value_serializer.rb +5 -4
  38. data/lib/playwright/javascript.rb +1 -0
  39. data/lib/playwright/locator_impl.rb +3 -5
  40. data/lib/playwright/route_handler.rb +2 -6
  41. data/lib/playwright/utils.rb +31 -6
  42. data/lib/playwright/version.rb +2 -2
  43. data/lib/playwright.rb +2 -0
  44. data/lib/playwright_api/accessibility.rb +8 -0
  45. data/lib/playwright_api/android_device.rb +5 -1
  46. data/lib/playwright_api/api_request_context.rb +8 -8
  47. data/lib/playwright_api/browser.rb +28 -2
  48. data/lib/playwright_api/browser_context.rb +19 -9
  49. data/lib/playwright_api/browser_type.rb +8 -2
  50. data/lib/playwright_api/download.rb +1 -1
  51. data/lib/playwright_api/element_handle.rb +1 -12
  52. data/lib/playwright_api/file_chooser.rb +1 -1
  53. data/lib/playwright_api/locator.rb +13 -13
  54. data/lib/playwright_api/page.rb +19 -6
  55. data/lib/playwright_api/request.rb +8 -1
  56. data/lib/playwright_api/response.rb +14 -1
  57. data/lib/playwright_api/route.rb +61 -2
  58. data/lib/playwright_api/selectors.rb +1 -1
  59. data/lib/playwright_api/tracing.rb +1 -1
  60. metadata +5 -4
  61. data/lib/playwright_api/local_utils.rb +0 -9
@@ -37,13 +37,11 @@ module Playwright
37
37
 
38
38
  # @param method [String]
39
39
  # @param params [Hash]
40
- # @returns nil
40
+ # @returns [Concurrent::Promises::Future]
41
41
  def async_send_message_to_server(method, params = {})
42
42
  with_logging do |metadata|
43
43
  @connection.async_send_message_to_server(@guid, method, params, metadata: metadata)
44
44
  end
45
-
46
- nil
47
45
  end
48
46
 
49
47
  private def with_logging(&block)
@@ -42,10 +42,16 @@ module Playwright
42
42
 
43
43
  attr_reader :channel
44
44
 
45
+ private def adopt!(child)
46
+ unless child.is_a?(ChannelOwner)
47
+ raise ArgumentError.new("child must be a ChannelOwner: #{child.inspect}")
48
+ end
49
+ child.send(:update_parent, self)
50
+ end
51
+
45
52
  # used only from Connection. Not intended for public use. So keep private.
46
53
  private def dispose!
47
54
  # Clean up from parent and connection.
48
- @parent&.send(:delete_object_from_child, @guid)
49
55
  @connection.send(:delete_object_from_channel_owner, @guid)
50
56
 
51
57
  # Dispose all children.
@@ -65,6 +71,12 @@ module Playwright
65
71
  private def after_initialize
66
72
  end
67
73
 
74
+ private def update_parent(new_parent)
75
+ @parent.send(:delete_object_from_child, @guid)
76
+ new_parent.send(:update_object_from_child, @guid, self)
77
+ @parent = new_parent
78
+ end
79
+
68
80
  private def update_object_from_child(guid, child)
69
81
  @objects[guid] = child
70
82
  end
@@ -5,6 +5,7 @@ module Playwright
5
5
  include Utils::PrepareBrowserContextOptions
6
6
 
7
7
  private def after_initialize
8
+ @browser_type = @parent
8
9
  @connected = true
9
10
  @closed_or_closing = false
10
11
  @should_close_connection_on_close = false
@@ -17,6 +18,10 @@ module Playwright
17
18
  @contexts.to_a
18
19
  end
19
20
 
21
+ def browser_type
22
+ @browser_type
23
+ end
24
+
20
25
  def connected?
21
26
  @connected
22
27
  end
@@ -30,6 +35,7 @@ module Playwright
30
35
  @contexts << context
31
36
  context.browser = self
32
37
  context.options = params
38
+ context.send(:update_browser_type, @browser_type)
33
39
  return context unless block
34
40
 
35
41
  begin
@@ -54,6 +60,13 @@ module Playwright
54
60
  end
55
61
  end
56
62
 
63
+ private def update_browser_type(browser_type)
64
+ @browser_type = browser_type
65
+ @contexts.each do |context|
66
+ context.send(:update_browser_type, browser_type)
67
+ end
68
+ end
69
+
57
70
  def close
58
71
  return if @closed_or_closing
59
72
  @closed_or_closing = true
@@ -2,6 +2,8 @@ module Playwright
2
2
  # @ref https://github.com/microsoft/playwright-python/blob/master/playwright/_impl/_browser_context.py
3
3
  define_channel_owner :BrowserContext do
4
4
  include Utils::Errors::SafeCloseError
5
+ include Utils::PrepareBrowserContextOptions
6
+
5
7
  attr_accessor :browser
6
8
  attr_writer :owner_page, :options
7
9
  attr_reader :tracing, :request
@@ -16,13 +18,18 @@ module Playwright
16
18
  @owner_page = nil
17
19
 
18
20
  @tracing = ChannelOwners::Tracing.from(@initializer['tracing'])
19
- @request = ChannelOwners::APIRequestContext.from(@initializer['APIRequestContext'])
21
+ @request = ChannelOwners::APIRequestContext.from(@initializer['requestContext'])
22
+ @har_recorders = {}
20
23
 
21
24
  @channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) })
22
25
  @channel.once('close', ->(_) { on_close })
23
26
  @channel.on('page', ->(params) { on_page(ChannelOwners::Page.from(params['page']) )})
24
27
  @channel.on('route', ->(params) {
25
- on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
28
+ Concurrent::Promises.future {
29
+ on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
30
+ }.rescue do |err|
31
+ puts err, err.backtrace
32
+ end
26
33
  })
27
34
  @channel.on('backgroundPage', ->(params) {
28
35
  on_background_page(ChannelOwners::Page.from(params['page']))
@@ -55,6 +62,16 @@ module Playwright
55
62
  @closed_promise = Concurrent::Promises.resolvable_future
56
63
  end
57
64
 
65
+ private def update_browser_type(browser_type)
66
+ @browser_type = browser_type
67
+ if @options[:recordHar]
68
+ @har_recorders[''] = {
69
+ path: @options[:recordHar][:path],
70
+ content: @options[:recordHar][:content]
71
+ }
72
+ end
73
+ end
74
+
58
75
  private def on_page(page)
59
76
  @pages << page
60
77
  emit(Events::BrowserContext::Page, page)
@@ -73,19 +90,29 @@ module Playwright
73
90
  wrapped_route = PlaywrightApi.wrap(route)
74
91
  wrapped_request = PlaywrightApi.wrap(request)
75
92
 
76
- handler_entry = @routes.find do |entry|
77
- entry.match?(request.url)
93
+ handled = @routes.any? do |handler_entry|
94
+ next false unless handler_entry.match?(request.url)
95
+
96
+ promise = Concurrent::Promises.resolvable_future
97
+ route.send(:set_handling_future, promise)
98
+
99
+ promise_handled = Concurrent::Promises.zip(
100
+ promise,
101
+ handler_entry.async_handle(wrapped_route, wrapped_request)
102
+ ).value!.first
103
+
104
+ promise_handled
78
105
  end
79
106
 
80
- if handler_entry
81
- handler_entry.async_handle(wrapped_route, wrapped_request)
107
+ @routes.reject!(&:expired?)
108
+ if @routes.count == 0
109
+ @channel.async_send_message_to_server('setNetworkInterceptionEnabled', enabled: false)
110
+ end
82
111
 
83
- @routes.reject!(&:expired?)
84
- if @routes.count == 0
85
- @channel.async_send_message_to_server('setNetworkInterceptionEnabled', enabled: false)
112
+ unless handled
113
+ route.send(:async_continue_route).rescue do |err|
114
+ puts err, err.backtrace
86
115
  end
87
- else
88
- route.continue
89
116
  end
90
117
  end
91
118
 
@@ -266,6 +293,37 @@ module Playwright
266
293
  end
267
294
  end
268
295
 
296
+ private def record_into_har(har, page, notFound:, url:)
297
+ params = {
298
+ options: prepare_record_har_options(
299
+ record_har_path: har,
300
+ record_har_content: "attach",
301
+ record_har_mode: "minimal",
302
+ record_har_url_filter: url,
303
+ )
304
+ }
305
+ if page
306
+ params[:page] = page.channel
307
+ end
308
+ har_id = @channel.send_message_to_server('harStart', params)
309
+ @har_recorders[har_id] = { path: har, content: 'attach' }
310
+ end
311
+
312
+ def route_from_har(har, notFound: nil, update: nil, url: nil)
313
+ if update
314
+ record_into_har(har, nil, notFound: notFound, url: url)
315
+ return
316
+ end
317
+
318
+ router = HarRouter.create(
319
+ @connection.local_utils,
320
+ har.to_s,
321
+ notFound || "abort",
322
+ url_match: url,
323
+ )
324
+ router.add_context_route(self)
325
+ end
326
+
269
327
  def expect_event(event, predicate: nil, timeout: nil, &block)
270
328
  wait_helper = WaitHelper.new
271
329
  wait_helper.reject_on_timeout(timeout || @timeout_settings.timeout, "Timeout while waiting for event \"#{event}\"")
@@ -283,9 +341,19 @@ module Playwright
283
341
  end
284
342
 
285
343
  def close
286
- if @options && @options.key?(:recordHar)
287
- har = ChannelOwners::Artifact.from(@channel.send_message_to_server('harExport'))
288
- har.save_as(@options[:recordHar][:path])
344
+ @har_recorders.each do |har_id, params|
345
+ har = ChannelOwners::Artifact.from(@channel.send_message_to_server('harExport', harId: har_id))
346
+ # Server side will compress artifact if content is attach or if file is .zip.
347
+ compressed = params[:content] == "attach" || params[:path].end_with?('.zip')
348
+ need_comppressed = params[:path].end_with?('.zip')
349
+ if compressed && !need_comppressed
350
+ tmp_path = "#{params[:path]}.tmp"
351
+ har.save_as(tmp_path)
352
+ @connection.local_utils.har_unzip(tmp_path, params[:path])
353
+ else
354
+ har.save_as(params[:path])
355
+ end
356
+
289
357
  har.delete
290
358
  end
291
359
  @channel.send_message_to_server('close')
@@ -13,6 +13,7 @@ module Playwright
13
13
  def launch(options, &block)
14
14
  resp = @channel.send_message_to_server('launch', options.compact)
15
15
  browser = ChannelOwners::Browser.from(resp)
16
+ browser.send(:update_browser_type, self)
16
17
  return browser unless block
17
18
 
18
19
  begin
@@ -29,6 +30,8 @@ module Playwright
29
30
 
30
31
  resp = @channel.send_message_to_server('launchPersistentContext', params.compact)
31
32
  context = ChannelOwners::Browser.from(resp)
33
+ context.options = params
34
+ context.send(:update_browser_type, self)
32
35
  return context unless block
33
36
 
34
37
  begin
@@ -54,6 +57,7 @@ module Playwright
54
57
 
55
58
  result = @channel.send_message_to_server_result('connectOverCDP', params)
56
59
  browser = ChannelOwners::Browser.from(result['browser'])
60
+ browser.send(:update_browser_type, self)
57
61
 
58
62
  if result['defaultContext']
59
63
  context = ChannelOwners::BrowserContext.from(result['defaultContext'])
@@ -25,11 +25,25 @@ module Playwright
25
25
  private def on_load_state(add:, remove:)
26
26
  if add
27
27
  @load_states << add
28
- @event_emitter.emit('loadstate', add)
28
+
29
+ # Original JS version of Playwright emit event here.
30
+ # @event_emitter.emit('loadstate', add)
29
31
  end
30
32
  if remove
31
33
  @load_states.delete(remove)
32
34
  end
35
+ unless @parent_frame
36
+ if add == 'load'
37
+ @page&.emit(Events::Page::Load, @page)
38
+ elsif add == 'domcontentloaded'
39
+ @page&.emit(Events::Page::DOMContentLoaded, @page)
40
+ end
41
+ end
42
+
43
+ # emit to waitForLoadState(load) listeners explicitly after waitForEvent(load) listeners
44
+ if add
45
+ @event_emitter.emit('loadstate', add)
46
+ end
33
47
  end
34
48
 
35
49
  private def on_frame_navigated(event)
@@ -38,7 +52,7 @@ module Playwright
38
52
  @event_emitter.emit('navigated', event)
39
53
 
40
54
  unless event['error']
41
- @page&.emit('framenavigated', self)
55
+ @page&.emit(Events::Page::FrameNavigated, self)
42
56
  end
43
57
  end
44
58
 
@@ -10,5 +10,34 @@ module Playwright
10
10
  @channel.send_message_to_server('zip', params)
11
11
  nil
12
12
  end
13
+
14
+
15
+ # @param file [String]
16
+ # @return [String] har ID
17
+ def har_open(file)
18
+ @channel.send_message_to_server('harOpen', file: file)
19
+ end
20
+
21
+ def har_lookup(har_id:, url:, method:, headers:, is_navigation_request:, post_data: nil)
22
+ params = {
23
+ harId: har_id,
24
+ url: url,
25
+ method: method,
26
+ headers: headers,
27
+ postData: post_data,
28
+ isNavigationRequest: is_navigation_request,
29
+ }.compact
30
+
31
+ @channel.send_message_to_server_result('harLookup', params)
32
+ end
33
+
34
+ # @param har_id [String]
35
+ def async_har_close(har_id)
36
+ @channel.async_send_message_to_server('harClose', harId: har_id)
37
+ end
38
+
39
+ def har_unzip(zip_file, har_file)
40
+ @channel.send_message_to_server('harUnzip', zipFile: zip_file, harFile: har_file)
41
+ end
13
42
  end
14
43
  end
@@ -39,7 +39,6 @@ module Playwright
39
39
  })
40
40
  @channel.on('crash', ->(_) { emit(Events::Page::Crash) })
41
41
  @channel.on('dialog', method(:on_dialog))
42
- @channel.on('domcontentloaded', ->(_) { emit(Events::Page::DOMContentLoaded) })
43
42
  @channel.on('download', method(:on_download))
44
43
  @channel.on('fileChooser', ->(params) {
45
44
  chooser = FileChooserImpl.new(
@@ -54,12 +53,15 @@ module Playwright
54
53
  @channel.on('frameDetached', ->(params) {
55
54
  on_frame_detached(ChannelOwners::Frame.from(params['frame']))
56
55
  })
57
- @channel.on('load', ->(_) { emit(Events::Page::Load) })
58
56
  @channel.on('pageError', ->(params) {
59
57
  emit(Events::Page::PageError, Error.parse(params['error']['error']))
60
58
  })
61
59
  @channel.on('route', ->(params) {
62
- on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
60
+ Concurrent::Promises.future {
61
+ on_route(ChannelOwners::Route.from(params['route']), ChannelOwners::Request.from(params['request']))
62
+ }.rescue do |err|
63
+ puts err, err.backtrace
64
+ end
63
65
  })
64
66
  @channel.on('video', method(:on_video))
65
67
  @channel.on('webSocket', ->(params) {
@@ -98,18 +100,26 @@ module Playwright
98
100
  wrapped_route = PlaywrightApi.wrap(route)
99
101
  wrapped_request = PlaywrightApi.wrap(request)
100
102
 
101
- handler_entry = @routes.find do |entry|
102
- entry.match?(request.url)
103
+ handled = @routes.any? do |handler_entry|
104
+ next false unless handler_entry.match?(request.url)
105
+
106
+ promise = Concurrent::Promises.resolvable_future
107
+ route.send(:set_handling_future, promise)
108
+
109
+ promise_handled = Concurrent::Promises.zip(
110
+ promise,
111
+ handler_entry.async_handle(wrapped_route, wrapped_request)
112
+ ).value!.first
113
+
114
+ promise_handled
103
115
  end
104
116
 
105
- if handler_entry
106
- handler_entry.async_handle(wrapped_route, wrapped_request)
117
+ @routes.reject!(&:expired?)
118
+ if @routes.count == 0
119
+ @channel.async_send_message_to_server('setNetworkInterceptionEnabled', enabled: false)
120
+ end
107
121
 
108
- @routes.reject!(&:expired?)
109
- if @routes.count == 0
110
- @channel.async_send_message_to_server('setNetworkInterceptionEnabled', enabled: false)
111
- end
112
- else
122
+ unless handled
113
123
  @browser_context.send(:on_route, route, request)
114
124
  end
115
125
  end
@@ -407,6 +417,21 @@ module Playwright
407
417
  end
408
418
  end
409
419
 
420
+ def route_from_har(har, notFound: nil, update: nil, url: nil)
421
+ if update
422
+ @browser_context.send(:record_into_har, har, self, notFound: notFound, url: url)
423
+ return
424
+ end
425
+
426
+ router = HarRouter.create(
427
+ @connection.local_utils,
428
+ har.to_s,
429
+ notFound || "abort",
430
+ url_match: url,
431
+ )
432
+ router.add_page_route(self)
433
+ end
434
+
410
435
  def screenshot(
411
436
  animations: nil,
412
437
  caret: nil,
@@ -451,9 +476,12 @@ module Playwright
451
476
  end
452
477
 
453
478
  def close(runBeforeUnload: nil)
454
- options = { runBeforeUnload: runBeforeUnload }.compact
455
- @channel.send_message_to_server('close', options)
456
- @owned_context&.close
479
+ if @owned_context
480
+ @owned_context.close
481
+ else
482
+ options = { runBeforeUnload: runBeforeUnload }.compact
483
+ @channel.send_message_to_server('close', options)
484
+ end
457
485
  nil
458
486
  rescue => err
459
487
  raise unless safe_close_error?(err)
@@ -18,10 +18,24 @@ module Playwright
18
18
  responseStart: -1,
19
19
  responseEnd: -1,
20
20
  }
21
+ @fallback_overrides = {}
22
+ end
23
+
24
+ private def fallback_overrides
25
+ @fallback_overrides
26
+ end
27
+
28
+ def apply_fallback_overrides(overrides)
29
+ allowed_key = %i[url method headers postData]
30
+ overrides.each do |key, value|
31
+ raise ArgumentError.new("invalid key: #{key}") unless allowed_key.include?(key)
32
+ @fallback_overrides[key] = value
33
+ end
34
+ @fallback_overrides
21
35
  end
22
36
 
23
37
  def url
24
- @initializer['url']
38
+ @fallback_overrides[:url] || @initializer['url']
25
39
  end
26
40
 
27
41
  def resource_type
@@ -29,7 +43,7 @@ module Playwright
29
43
  end
30
44
 
31
45
  def method
32
- @initializer['method']
46
+ @fallback_overrides[:method] || @initializer['method']
33
47
  end
34
48
 
35
49
  def post_data
@@ -51,8 +65,11 @@ module Playwright
51
65
  end
52
66
 
53
67
  def post_data_buffer
54
- base64_content = @initializer['postData']
55
- if base64_content
68
+ if (override = @fallback_overrides[:postData])
69
+ return override
70
+ end
71
+
72
+ if (base64_content = @initializer['postData'])
56
73
  Base64.strict_decode64(base64_content)
57
74
  else
58
75
  nil
@@ -79,12 +96,20 @@ module Playwright
79
96
  attr_reader :redirected_from, :redirected_to, :timing
80
97
 
81
98
  def headers
82
- @provisional_headers.headers
99
+ if (override = @fallback_overrides[:headers])
100
+ RawHeaders.new(HttpHeaders.new(override).as_serialized).headers
101
+ else
102
+ @provisional_headers.headers
103
+ end
83
104
  end
84
105
 
85
106
  # @return [RawHeaders|nil]
86
107
  private def actual_headers
87
- @actual_headers ||= raw_request_headers
108
+ if (override = @fallback_overrides[:headers])
109
+ RawHeaders.new(HttpHeaders.new(override).as_serialized)
110
+ else
111
+ @actual_headers ||= raw_request_headers
112
+ end
88
113
  end
89
114
 
90
115
  private def raw_request_headers
@@ -52,6 +52,12 @@ module Playwright
52
52
  RawHeaders.new(@channel.send_message_to_server('rawResponseHeaders'))
53
53
  end
54
54
 
55
+ def from_service_worker
56
+ @initializer['fromServiceWorker']
57
+ end
58
+
59
+ alias_method :from_service_worker?, :from_service_worker
60
+
55
61
  def all_headers
56
62
  actual_headers.headers
57
63
  end