playwright-ruby-client 1.59.1 → 1.60.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/documentation/docs/api/api_request.md +25 -1
  3. data/documentation/docs/api/api_request_context.md +13 -11
  4. data/documentation/docs/api/browser.md +18 -0
  5. data/documentation/docs/api/browser_context.md +1 -1
  6. data/documentation/docs/api/browser_type.md +18 -0
  7. data/documentation/docs/api/frame.md +1 -0
  8. data/documentation/docs/api/frame_locator.md +1 -0
  9. data/documentation/docs/api/locator.md +37 -2
  10. data/documentation/docs/api/locator_assertions.md +1 -1
  11. data/documentation/docs/api/page.md +12 -2
  12. data/documentation/docs/api/page_assertions.md +28 -0
  13. data/documentation/docs/api/playwright.md +5 -0
  14. data/documentation/docs/api/tracing.md +29 -0
  15. data/documentation/docs/include/api_coverage.md +14 -60
  16. data/lib/playwright/api_request_impl.rb +70 -0
  17. data/lib/playwright/channel_owners/api_request_context.rb +5 -1
  18. data/lib/playwright/channel_owners/binding_call.rb +3 -9
  19. data/lib/playwright/channel_owners/browser.rb +17 -0
  20. data/lib/playwright/channel_owners/browser_context.rb +10 -54
  21. data/lib/playwright/channel_owners/browser_type.rb +34 -1
  22. data/lib/playwright/channel_owners/frame.rb +89 -3
  23. data/lib/playwright/channel_owners/json_pipe.rb +4 -0
  24. data/lib/playwright/channel_owners/local_utils.rb +11 -2
  25. data/lib/playwright/channel_owners/page.rb +16 -11
  26. data/lib/playwright/channel_owners/playwright.rb +4 -0
  27. data/lib/playwright/channel_owners/tracing.rb +105 -4
  28. data/lib/playwright/connection.rb +5 -1
  29. data/lib/playwright/console_message_impl.rb +10 -1
  30. data/lib/playwright/errors.rb +3 -2
  31. data/lib/playwright/events.rb +7 -0
  32. data/lib/playwright/json_pipe_transport.rb +40 -0
  33. data/lib/playwright/locator_assertions_impl.rb +21 -4
  34. data/lib/playwright/locator_impl.rb +17 -3
  35. data/lib/playwright/locator_utils.rb +2 -0
  36. data/lib/playwright/page_assertions_impl.rb +33 -3
  37. data/lib/playwright/screencast.rb +5 -1
  38. data/lib/playwright/url_matcher.rb +35 -0
  39. data/lib/playwright/version.rb +2 -2
  40. data/lib/playwright.rb +1 -0
  41. data/lib/playwright_api/api_request.rb +1 -1
  42. data/lib/playwright_api/api_request_context.rb +15 -11
  43. data/lib/playwright_api/browser.rb +2 -2
  44. data/lib/playwright_api/browser_context.rb +7 -7
  45. data/lib/playwright_api/browser_type.rb +5 -3
  46. data/lib/playwright_api/frame.rb +18 -7
  47. data/lib/playwright_api/frame_locator.rb +2 -1
  48. data/lib/playwright_api/locator.rb +34 -5
  49. data/lib/playwright_api/locator_assertions.rb +2 -2
  50. data/lib/playwright_api/page.rb +27 -20
  51. data/lib/playwright_api/page_assertions.rb +22 -0
  52. data/lib/playwright_api/playwright.rb +1 -1
  53. data/lib/playwright_api/tracing.rb +23 -0
  54. data/sig/playwright.rbs +35 -43
  55. metadata +5 -12
  56. data/documentation/docs/api/experimental/android.md +0 -42
  57. data/documentation/docs/api/experimental/android_device.md +0 -109
  58. data/documentation/docs/api/experimental/android_input.md +0 -43
  59. data/documentation/docs/api/experimental/android_socket.md +0 -7
  60. data/documentation/docs/api/experimental/android_web_view.md +0 -7
  61. data/lib/playwright_api/android.rb +0 -68
  62. data/lib/playwright_api/android_device.rb +0 -229
  63. data/lib/playwright_api/android_input.rb +0 -34
  64. data/lib/playwright_api/android_socket.rb +0 -18
  65. data/lib/playwright_api/android_web_view.rb +0 -24
@@ -20,7 +20,6 @@ module Playwright
20
20
  @request = ChannelOwners::APIRequestContext.from(@initializer['requestContext'])
21
21
  @request.send(:_update_timeout_settings, @timeout_settings)
22
22
  @clock = ClockImpl.new(self)
23
- @har_recorders = {}
24
23
 
25
24
  @channel.on('bindingCall', ->(params) { on_binding(ChannelOwners::BindingCall.from(params['binding'])) })
26
25
  @channel.once('close', ->(_) { on_close })
@@ -34,8 +33,9 @@ module Playwright
34
33
  })
35
34
  @channel.on('pageError', ->(params) {
36
35
  on_page_error(
37
- Error.parse(params['error']['error']),
36
+ Error.parse(params.dig('error', 'error') || params['error']),
38
37
  ChannelOwners::Page.from_nullable(params['page']),
38
+ params['location'],
39
39
  )
40
40
  })
41
41
  @channel.on('dialog', ->(params) {
@@ -82,7 +82,7 @@ module Playwright
82
82
 
83
83
  default_policy = record_har_path.end_with?('.zip') ? 'attach' : 'embed'
84
84
  content_policy = record_har_content || (record_har_omit_content ? 'omit' : default_policy)
85
- record_into_har(record_har_path, nil,
85
+ @tracing.send(:record_into_har, record_har_path, nil,
86
86
  url: record_har_url_filter,
87
87
  update_content: content_policy,
88
88
  update_mode: record_har_mode || 'full',
@@ -186,8 +186,8 @@ module Playwright
186
186
  end
187
187
  end
188
188
 
189
- private def on_page_error(error, page)
190
- emit(Events::BrowserContext::WebError, WebError.new(error, page))
189
+ private def on_page_error(error, page, location)
190
+ emit(Events::BrowserContext::WebError, WebError.new(error, page, location))
191
191
  if page
192
192
  page.emit(Events::Page::PageError, error)
193
193
  end
@@ -337,19 +337,15 @@ module Playwright
337
337
  ChannelOwners::Disposable.from(result['disposable'])
338
338
  end
339
339
 
340
- def expose_binding(name, callback, handle: nil)
340
+ def expose_binding(name, callback)
341
341
  if @pages.any? { |page| page.send(:has_bindings?, name) }
342
342
  raise ArgumentError.new("Function \"#{name}\" has been already registered in one of the pages")
343
343
  end
344
344
  if @bindings.key?(name)
345
345
  raise ArgumentError.new("Function \"#{name}\" has been already registered")
346
346
  end
347
- params = {
348
- name: name,
349
- needsHandle: handle,
350
- }.compact
351
347
  @bindings[name] = callback
352
- result = @channel.send_message_to_server_result('exposeBinding', params)
348
+ result = @channel.send_message_to_server_result('exposeBinding', name: name)
353
349
  ChannelOwners::Disposable.from(result['disposable'])
354
350
  end
355
351
 
@@ -376,32 +372,9 @@ module Playwright
376
372
  update_interception_patterns
377
373
  end
378
374
 
379
- private def record_into_har(har, page, url:, update_content:, update_mode:)
380
- options = {
381
- zip: har.end_with?('.zip'),
382
- content: update_content || 'attach',
383
- }
384
-
385
- if url.is_a?(Regexp)
386
- regex = ::Playwright::JavaScript::Regex.new(url)
387
- options[:urlRegexSource] = regex.source
388
- options[:urlRegexFlags] = regex.flag
389
- elsif url.is_a?(String)
390
- options[:urlGlob] = url
391
- end
392
-
393
- params = { options: options }
394
- if page
395
- params[:page] = page.channel
396
- end
397
-
398
- har_id = @channel.send_message_to_server('harStart', params)
399
- @har_recorders[har_id] = { path: har, content: update_content || 'attach' }
400
- end
401
-
402
375
  def route_from_har(har, notFound: nil, update: nil, updateContent: nil, updateMode: nil, url: nil)
403
376
  if update
404
- record_into_har(har, nil, url: url, update_content: updateContent, update_mode: updateMode)
377
+ @tracing.send(:record_into_har, har, nil, url: url, update_content: updateContent, update_mode: updateMode)
405
378
  return
406
379
  end
407
380
 
@@ -443,6 +416,7 @@ module Playwright
443
416
  @browser.send(:remove_context, self)
444
417
  @browser.browser_type.send(:playwright_selectors_browser_contexts).delete(self)
445
418
  end
419
+ @tracing.send(:reset_stack_counter)
446
420
  emit(Events::BrowserContext::Close)
447
421
  @closed_promise.fulfill(true)
448
422
  end
@@ -452,30 +426,12 @@ module Playwright
452
426
  @close_was_called = true
453
427
  @close_reason = reason
454
428
  @request.dispose(reason: reason)
455
- inner_close
429
+ @tracing.send(:export_all_hars)
456
430
  @channel.send_message_to_server('close', { reason: reason }.compact)
457
431
  @closed_promise.value!
458
432
  nil
459
433
  end
460
434
 
461
- private def inner_close
462
- @har_recorders.each do |har_id, params|
463
- har = ChannelOwners::Artifact.from(@channel.send_message_to_server('harExport', harId: har_id))
464
- # Server side will compress artifact if content is attach or if file is .zip.
465
- compressed = params[:content] == "attach" || params[:path].end_with?('.zip')
466
- need_comppressed = params[:path].end_with?('.zip')
467
- if compressed && !need_comppressed
468
- tmp_path = "#{params[:path]}.tmp"
469
- har.save_as(tmp_path)
470
- @connection.local_utils.har_unzip(tmp_path, params[:path])
471
- else
472
- har.save_as(params[:path])
473
- end
474
-
475
- har.delete
476
- end
477
- end
478
-
479
435
  # REMARK: enable_debug_console is playwright-ruby-client specific method.
480
436
  def enable_debug_console!
481
437
  # Ruby is not supported in Playwright officially,
@@ -43,6 +43,7 @@ module Playwright
43
43
  browser = ChannelOwners::Browser.from(result['browser'])
44
44
  browser.send(:connect_to_browser_type, self, params[:tracesDir])
45
45
  context = ChannelOwners::BrowserContext.from(result['context'])
46
+ context.send(:update_options, context_options: params, browser_options: params)
46
47
  context.send(:initialize_har_from_options,
47
48
  record_har_content: params[:record_har_content],
48
49
  record_har_mode: params[:record_har_mode],
@@ -60,13 +61,45 @@ module Playwright
60
61
  end
61
62
  end
62
63
 
63
- def connect_over_cdp(endpointURL, headers: nil, isLocal: nil, slowMo: nil, timeout: nil, &block)
64
+ def connect(endpoint, exposeNetwork: nil, headers: nil, slowMo: nil, timeout: nil, &block)
65
+ params = {
66
+ endpoint: endpoint,
67
+ headers: { 'x-playwright-browser' => name }.merge(headers || {}),
68
+ exposeNetwork: exposeNetwork,
69
+ slowMo: slowMo,
70
+ timeout: @timeout_settings.timeout(timeout),
71
+ }.compact
72
+
73
+ transport = JsonPipeTransport.new(@connection.local_utils, params)
74
+ connection = Connection.new(transport)
75
+ connection.mark_as_remote
76
+ connection.async_run
77
+
78
+ playwright = connection.initialize_playwright
79
+ browser = playwright.send(:pre_launched_browser)
80
+ browser.send(:should_close_connection_on_close!)
81
+ browser.send(:connect_to_browser_type, self, nil)
82
+
83
+ return browser unless block
84
+
85
+ begin
86
+ block.call(browser)
87
+ ensure
88
+ browser.close
89
+ end
90
+ rescue
91
+ connection&.stop
92
+ raise
93
+ end
94
+
95
+ def connect_over_cdp(endpointURL, headers: nil, isLocal: nil, noDefaults: nil, slowMo: nil, timeout: nil, &block)
64
96
  raise 'Connecting over CDP is only supported in Chromium.' unless name == 'chromium'
65
97
 
66
98
  params = {
67
99
  endpointURL: endpointURL,
68
100
  headers: headers,
69
101
  isLocal: isLocal,
102
+ noDefaults: noDefaults,
70
103
  slowMo: slowMo,
71
104
  timeout: @timeout_settings.timeout(timeout),
72
105
  }.compact
@@ -1,3 +1,4 @@
1
+ require 'base64'
1
2
  require_relative '../locator_utils'
2
3
 
3
4
  module Playwright
@@ -47,6 +48,7 @@ module Playwright
47
48
  unless @parent_frame
48
49
  if add == 'load'
49
50
  @page&.emit(Events::Page::Load, @page)
51
+ @page&.context&.emit(Events::BrowserContext::PageLoad, @page)
50
52
  elsif add == 'domcontentloaded'
51
53
  @page&.emit(Events::Page::DOMContentLoaded, @page)
52
54
  end
@@ -65,6 +67,7 @@ module Playwright
65
67
 
66
68
  unless event['error']
67
69
  @page&.emit(Events::Page::FrameNavigated, self)
70
+ @page&.context&.emit(Events::BrowserContext::FrameNavigated, self)
68
71
  end
69
72
  end
70
73
 
@@ -388,6 +391,23 @@ module Playwright
388
391
  nil
389
392
  end
390
393
 
394
+ def drop(
395
+ selector,
396
+ payload,
397
+ position: nil,
398
+ strict: nil,
399
+ timeout: nil)
400
+ params = {
401
+ selector: selector,
402
+ position: position,
403
+ strict: strict,
404
+ timeout: _timeout(timeout),
405
+ }.compact.merge(drop_payload_params(payload))
406
+ @channel.send_message_to_server('drop', params)
407
+
408
+ nil
409
+ end
410
+
391
411
  def dblclick(
392
412
  selector,
393
413
  button: nil,
@@ -690,8 +710,12 @@ module Playwright
690
710
  @channel.send_message_to_server('title')
691
711
  end
692
712
 
693
- def highlight(selector)
694
- @channel.send_message_to_server('highlight', selector: selector)
713
+ def highlight(selector, style: nil)
714
+ @channel.send_message_to_server('highlight', { selector: selector, style: style }.compact)
715
+ end
716
+
717
+ def hide_highlight(selector)
718
+ @channel.send_message_to_server('hideHighlight', selector: selector)
695
719
  end
696
720
 
697
721
  def expect(selector, expression, options, title)
@@ -712,12 +736,74 @@ module Playwright
712
736
  )
713
737
 
714
738
  if result.key?('received')
715
- result['received'] = JavaScript::ValueParser.new(result['received']).parse
739
+ if result['received'].is_a?(Hash) && result['received'].key?('value')
740
+ result['received']['value'] = JavaScript::ValueParser.new(result['received']['value']).parse
741
+ elsif !result['received'].is_a?(Hash)
742
+ result['received'] = JavaScript::ValueParser.new(result['received']).parse
743
+ end
716
744
  end
717
745
 
718
746
  result
719
747
  end
720
748
 
749
+ private def drop_payload_params(payload)
750
+ unless payload.is_a?(Hash)
751
+ raise ArgumentError.new('payload must be a Hash')
752
+ end
753
+ unless payload.key?(:files) || payload.key?('files') || payload.key?(:data) || payload.key?('data')
754
+ raise ArgumentError.new('At least one of "files" or "data" must be specified')
755
+ end
756
+
757
+ params = {}
758
+ files = payload[:files] || payload['files']
759
+ params[:payloads] = drop_file_payloads(files) if files
760
+
761
+ data = payload[:data] || payload['data']
762
+ if data
763
+ params[:data] = data.map do |mime_type, value|
764
+ { mimeType: mime_type, value: value }
765
+ end
766
+ end
767
+
768
+ params
769
+ end
770
+
771
+ private def drop_file_payloads(files)
772
+ items =
773
+ if files.is_a?(Array)
774
+ files
775
+ else
776
+ [files]
777
+ end
778
+
779
+ if items.any? { |item| item.is_a?(String) || item.is_a?(File) }
780
+ unless items.all? { |item| item.is_a?(String) || item.is_a?(File) }
781
+ raise ArgumentError.new('File paths cannot be mixed with buffers')
782
+ end
783
+ return items.map do |item|
784
+ path = item.is_a?(File) ? item.path : item
785
+ raise ArgumentError.new('Dropping a directory is not supported - pass individual files.') if File.directory?(path)
786
+
787
+ {
788
+ name: File.basename(path),
789
+ buffer: Base64.strict_encode64(File.binread(path)),
790
+ }
791
+ end
792
+ end
793
+
794
+ items.map do |item|
795
+ unless item.is_a?(Hash)
796
+ raise ArgumentError.new('files must be file paths or file payload hashes')
797
+ end
798
+ buffer = item[:buffer] || item['buffer']
799
+ {
800
+ name: item[:name] || item['name'],
801
+ mimeType: item[:mimeType] || item['mimeType'],
802
+ buffer: Base64.strict_encode64(buffer),
803
+ }.compact
804
+ end
805
+ end
806
+
721
807
  # @param page [Page]
722
808
  # @note This method should be used internally. Accessed via .send method, so keep private!
723
809
  private def update_page_from_page(page)
@@ -0,0 +1,4 @@
1
+ module Playwright
2
+ define_channel_owner :JsonPipe do
3
+ end
4
+ end
@@ -37,8 +37,12 @@ module Playwright
37
37
  @channel.async_send_message_to_server('harClose', harId: har_id)
38
38
  end
39
39
 
40
- def har_unzip(zip_file, har_file)
41
- @channel.send_message_to_server('harUnzip', zipFile: zip_file, harFile: har_file)
40
+ def har_unzip(zip_file, har_file, resources_dir: nil)
41
+ @channel.send_message_to_server('harUnzip', {
42
+ zipFile: zip_file,
43
+ harFile: har_file,
44
+ resourcesDir: resources_dir,
45
+ }.compact)
42
46
  end
43
47
 
44
48
  def tracing_started(traces_dir, trace_name)
@@ -58,6 +62,11 @@ module Playwright
58
62
  nil
59
63
  end
60
64
 
65
+ def connect(params)
66
+ result = @channel.send_message_to_server_result('connect', params)
67
+ ChannelOwners::JsonPipe.from(result['pipe'])
68
+ end
69
+
61
70
  private def parse_device_descriptor(descriptor)
62
71
  # This return value can be passed into Browser#new_context as it is.
63
72
  # ex:
@@ -53,7 +53,7 @@ module Playwright
53
53
  on_frame_detached(ChannelOwners::Frame.from(params['frame']))
54
54
  })
55
55
  @channel.on('pageError', ->(params) {
56
- emit(Events::Page::PageError, Error.parse(params['error']['error']))
56
+ emit(Events::Page::PageError, Error.parse(params.dig('error', 'error') || params['error']))
57
57
  })
58
58
  @channel.on('route', ->(params) { on_route(ChannelOwners::Route.from(params['route'])) })
59
59
  if @initializer['video']
@@ -90,12 +90,14 @@ module Playwright
90
90
  frame.send(:update_page_from_page, self)
91
91
  @frames << frame
92
92
  emit(Events::Page::FrameAttached, frame)
93
+ @browser_context.emit(Events::BrowserContext::FrameAttached, frame)
93
94
  end
94
95
 
95
96
  private def on_frame_detached(frame)
96
97
  @frames.delete(frame)
97
98
  frame.detached = true
98
99
  emit(Events::Page::FrameDetached, frame)
100
+ @browser_context.emit(Events::BrowserContext::FrameDetached, frame)
99
101
  end
100
102
 
101
103
  private def on_route(route)
@@ -153,6 +155,7 @@ module Playwright
153
155
  @closed_or_crashed_promise.fulfill(close_error_with_reason)
154
156
  end
155
157
  emit(Events::Page::Close)
158
+ @browser_context.emit(Events::BrowserContext::PageClose, self)
156
159
  end
157
160
 
158
161
  private def on_crash
@@ -171,6 +174,7 @@ module Playwright
171
174
  artifact: artifact,
172
175
  )
173
176
  emit(Events::Page::Download, download)
177
+ @browser_context.emit(Events::BrowserContext::Download, download)
174
178
  end
175
179
 
176
180
  private def on_viewport_size_changed(params)
@@ -309,12 +313,8 @@ module Playwright
309
313
  ChannelOwners::Disposable.from(result['disposable'])
310
314
  end
311
315
 
312
- def expose_binding(name, callback, handle: nil)
313
- params = {
314
- name: name,
315
- needsHandle: handle,
316
- }.compact
317
- result = @channel.send_message_to_server_result('exposeBinding', params)
316
+ def expose_binding(name, callback)
317
+ result = @channel.send_message_to_server_result('exposeBinding', name: name)
318
318
  @bindings[name] = callback
319
319
  ChannelOwners::Disposable.from(result['disposable'])
320
320
  end
@@ -436,7 +436,7 @@ module Playwright
436
436
 
437
437
  def route_from_har(har, notFound: nil, update: nil, url: nil, updateContent: nil, updateMode: nil)
438
438
  if update
439
- @browser_context.send(:record_into_har, har, self, url: url, update_content: updateContent, update_mode: updateMode)
439
+ @browser_context.tracing.send(:record_into_har, har, self, url: url, update_content: updateContent, update_mode: updateMode)
440
440
  return
441
441
  end
442
442
 
@@ -676,9 +676,14 @@ module Playwright
676
676
  @channel.send_message_to_server('cancelPickLocator')
677
677
  end
678
678
 
679
- def aria_snapshot(depth: nil, mode: nil, timeout: nil, _track: nil)
679
+ def hide_highlight
680
+ @channel.send_message_to_server('hideHighlight')
681
+ end
682
+
683
+ def aria_snapshot(boxes: nil, depth: nil, mode: nil, timeout: nil, _track: nil)
680
684
  params = { selector: 'body' }
681
685
  params[:timeout] = @timeout_settings.timeout(timeout)
686
+ params[:boxes] = boxes unless boxes.nil?
682
687
  params[:depth] = depth if depth
683
688
  params[:mode] = mode if mode
684
689
  if _track
@@ -947,8 +952,8 @@ module Playwright
947
952
  @main_frame.locator(result['selector'])
948
953
  end
949
954
 
950
- def snapshot_for_ai(timeout: nil, depth: nil, _track: nil)
951
- aria_snapshot(mode: 'ai', timeout: timeout, depth: depth, _track: _track)
955
+ def snapshot_for_ai(timeout: nil, depth: nil, boxes: nil, _track: nil)
956
+ aria_snapshot(mode: 'ai', timeout: timeout, depth: depth, boxes: boxes, _track: _track)
952
957
  end
953
958
 
954
959
  def _assertions(timeout, is_not, message)
@@ -34,6 +34,10 @@ module Playwright
34
34
  @connection.local_utils.devices
35
35
  end
36
36
 
37
+ def request
38
+ @request ||= APIRequestImpl.new(self)
39
+ end
40
+
37
41
  # used only from Playwright#connect_to_browser_server
38
42
  private def pre_launched_browser
39
43
  unless @initializer['preLaunchedBrowser']
@@ -1,5 +1,10 @@
1
1
  module Playwright
2
2
  define_channel_owner :Tracing do
3
+ private def after_initialize
4
+ @har_recorders = {}
5
+ @har_id = nil
6
+ end
7
+
3
8
  def start(name: nil, title: nil, screenshots: nil, snapshots: nil, sources: nil, live: nil)
4
9
  params = {
5
10
  name: name,
@@ -38,10 +43,7 @@ module Playwright
38
43
  end
39
44
 
40
45
  private def do_stop_chunk(file_path:)
41
- if @is_tracing
42
- @is_tracing = false
43
- @connection.set_in_tracing(false)
44
- end
46
+ reset_stack_counter
45
47
  local_utils = @connection.local_utils
46
48
 
47
49
  unless file_path
@@ -99,6 +101,105 @@ module Playwright
99
101
  )
100
102
  end
101
103
 
104
+ def start_har(path, content: nil, mode: nil, urlFilter: nil, resourcesDir: nil)
105
+ raise 'HAR recording has already been started' if @har_id
106
+ if resourcesDir && path.end_with?('.zip')
107
+ raise 'resourcesDir option is not compatible with a .zip har file'
108
+ end
109
+
110
+ default_content = path.end_with?('.zip') ? 'attach' : 'embed'
111
+ @har_id = record_into_har(path, nil,
112
+ url: urlFilter,
113
+ update_content: content || default_content,
114
+ update_mode: mode || 'full',
115
+ resources_dir: resourcesDir,
116
+ )
117
+ DisposableStub.new { stop_har }
118
+ end
119
+
120
+ def stop_har
121
+ har_id = @har_id
122
+ raise 'HAR recording has not been started' unless har_id
123
+
124
+ @har_id = nil
125
+ export_har(har_id)
126
+ nil
127
+ end
128
+
129
+ private def record_into_har(har, page, url:, update_content:, update_mode:, resources_dir: nil)
130
+ options = {
131
+ content: update_content || 'attach',
132
+ mode: update_mode || 'minimal',
133
+ harPath: har.end_with?('.zip') ? nil : har,
134
+ resourcesDir: resources_dir,
135
+ }.compact
136
+
137
+ if url.is_a?(Regexp)
138
+ regex = ::Playwright::JavaScript::Regex.new(url)
139
+ options[:urlRegexSource] = regex.source
140
+ options[:urlRegexFlags] = regex.flag
141
+ elsif url.is_a?(String)
142
+ options[:urlGlob] = url
143
+ end
144
+
145
+ params = { options: options }
146
+ params[:page] = page.channel if page
147
+
148
+ result = @channel.send_message_to_server_result('harStart', params)
149
+ har_id = result['harId'] || result[:harId]
150
+ @har_recorders[har_id] = { path: har, resources_dir: resources_dir }
151
+ har_id
152
+ end
153
+
154
+ private def export_har(har_id)
155
+ har_params = @har_recorders.delete(har_id)
156
+ return unless har_params
157
+
158
+ path = har_params[:path]
159
+ is_zip = path.end_with?('.zip')
160
+ local_utils = @connection.local_utils
161
+
162
+ if !@connection.remote?
163
+ result = @channel.send_message_to_server_result('harExport', harId: har_id, mode: 'entries')
164
+ return unless is_zip
165
+ raise 'Cannot save zipped HAR because localUtils is unavailable.' unless local_utils
166
+
167
+ local_utils.zip(
168
+ zipFile: path,
169
+ entries: result['entries'],
170
+ mode: 'write',
171
+ includeSources: false,
172
+ )
173
+ return
174
+ end
175
+
176
+ result = @channel.send_message_to_server_result('harExport', harId: har_id, mode: 'archive')
177
+ artifact = ChannelOwners::Artifact.from(result['artifact'])
178
+ if is_zip
179
+ artifact.save_as(path)
180
+ artifact.delete
181
+ return
182
+ end
183
+
184
+ raise 'Uncompressed har is not supported in thin clients' unless local_utils
185
+
186
+ tmp_path = "#{path}.tmp"
187
+ artifact.save_as(tmp_path)
188
+ local_utils.har_unzip(tmp_path, path, resources_dir: har_params[:resources_dir])
189
+ artifact.delete
190
+ end
191
+
192
+ private def export_all_hars
193
+ @har_recorders.keys.each { |har_id| export_har(har_id) }
194
+ end
195
+
196
+ private def reset_stack_counter
197
+ if @is_tracing
198
+ @is_tracing = false
199
+ @connection.set_in_tracing(false)
200
+ end
201
+ end
202
+
102
203
  private def update_traces_dir(traces_dir)
103
204
  @traces_dir = traces_dir
104
205
  end
@@ -213,7 +213,11 @@ module Playwright
213
213
  return
214
214
  end
215
215
 
216
- object.channel.emit(method, replace_guids_with_channels(params))
216
+ if object.is_a?(ChannelOwners::JsonPipe) && method == 'message'
217
+ object.channel.emit(method, params)
218
+ else
219
+ object.channel.emit(method, replace_guids_with_channels(params))
220
+ end
217
221
  end
218
222
 
219
223
  def replace_channels_with_guids(payload)
@@ -23,7 +23,16 @@ module Playwright
23
23
  end
24
24
 
25
25
  def location
26
- @event['location']
26
+ location = @event['location']
27
+ return location unless location
28
+
29
+ {
30
+ 'url' => location['url'],
31
+ 'line' => location['lineNumber'],
32
+ 'column' => location['columnNumber'],
33
+ 'lineNumber' => location['lineNumber'],
34
+ 'columnNumber' => location['columnNumber'],
35
+ }
27
36
  end
28
37
 
29
38
  def timestamp
@@ -60,12 +60,13 @@ module Playwright
60
60
  end
61
61
 
62
62
  class WebError
63
- def initialize(error, page)
63
+ def initialize(error, page, location = nil)
64
64
  @error = error
65
65
  @page = PlaywrightApi.wrap(page)
66
+ @location = location
66
67
  end
67
68
 
68
- attr_reader :error, :page
69
+ attr_reader :error, :page, :location
69
70
  end
70
71
 
71
72
  class AssertionError < StandardError; end
@@ -20,6 +20,7 @@ end
20
20
  },
21
21
 
22
22
  Browser: {
23
+ Context: 'context',
23
24
  Disconnected: 'disconnected'
24
25
  },
25
26
 
@@ -28,7 +29,13 @@ end
28
29
  Close: 'close',
29
30
  Console: 'console',
30
31
  Dialog: 'dialog',
32
+ Download: 'download',
33
+ FrameAttached: 'frameattached',
34
+ FrameDetached: 'framedetached',
35
+ FrameNavigated: 'framenavigated',
31
36
  Page: 'page',
37
+ PageClose: 'pageclose',
38
+ PageLoad: 'pageload',
32
39
  WebError: 'weberror',
33
40
  ServiceWorker: 'serviceworker',
34
41
  Request: 'request',